02基础语法--005--React 技术详解

2020-09-03  本文已影响0人  修_远
目录

(一)React 简介

DOM,文档对象模式

W3C组织推荐的处理可扩展标志语言的标准编程接口

DOM

1. 传统的HTML 网页开发

  1. 直接操作 DOM,需要非常大的开销;
  2. 更新页面内容或元素,需要将整个页面重新绘制;

2. React 性能优化方案(刷新逻辑)

  1. React 底层设计了一个虚拟的 DOM;
  2. 虚拟的 DOM 与页面真实的 DOM 进行映射;
  3. 当数据变化时
    1. React 会重新构建 DOM 树;
    2. 通过底层的 diff 算法找到 DOM 的差异部分;
    3. 浏览器只需要更新变化的部分;

3. React 的跨平台方案

借助虚拟的 DOM 技术来实现服务端应用、Web 应用和移动手机应用的跨平台开发

4. React 数据的单向流向

  1. 数据默认从父节点传到子节点;
  2. 父节点数据通过 props 传递到子节点,如果父节点的 props 值发生改变,那么其所有子节点也会执行重新渲染操作;
  3. 好处:使得组件足够扁平,更加便于维护。

(二)React 组件详解

2.1 React 组件基础知识

组件定义

组件分类:无状态组件

没有状态的组件,只做纯静态展示

组件分类:有状态组件

在无状态的组件的基础上增加了组件内部状态管理

组件的创建

import React, { Component } from 'react';

class TextView extends Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <div>Text</div>
        );
    }
}
const Todo = (props) => {
    <li 
        onClick={props.onClick}
        style={{textDecoration: props.complete ? "line-through" : "none"}}>
        {props.text}
    </li>
}
const Todo = ({ onClick, complete, text, props }) => {
    <li 
        onClick={props.onClick}
        style={{textDecoration: props.complete ? "line-through" : "none"}}>
        {props.text}
    </li>
}

【注意】

2.2 props

props 的使用方式

{this.props.key}

props 是父子组件交互的唯一方式:super(props);

const { Component } = require("react");

class HelloMessage extends Component {
    constructor(props) {
        super(props);
        this.state = {
            name = 'Jack'
        };
    }

    render() {
        return (
            <h1>Hello {this.props.name}</h1>
        )
    }
}

export default HelloMessage;

在子类中定义props

const { Component } = require("react");

export default class Child extends Component {
    constructor(props) {
        super(props);
        this.state = {
            counter:props.age||0
        };
    }

    render() {
        return (
            <h1>Hello {this.props.name}</h1>
        )
    }
}

Child.PropTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number
}

Child.defaultProps = {
    age: 0
}

在父类中使用 props

export default class Father extends Comment {
    render() {
        return (
            <div>
                <Child name="Jack" age={20} />
                <Child name="Tom" age={30} />
            </div>
        )
    }
}

2.3 state

组件“状态机”

通过与用户交互实现不同状态,进而渲染界面,让用户界面与数据保持一致

在 React 中,如果需要使用 state,就需要在组件的 constructor 中初始化相关的 state

constructor(props) {
    super(props);
    this.state() = {
        key:value,
        ...
    };
}

setState():更新组件的state

this.setState({
    key:value,
});

setState() 异步操作

  1. 更新的状态不会立马刷新,而是将修改的状态放入一个队列中;
  2. React 可能会对多次 setState 状态修改进行合并修正
  3. {this.state} 获取的状态可能会不准确;
  4. 也不能依赖 props 来计算组件的下一个状态;

setState 浅合并过程

在调用 setState 修改组件状态时,只需要传入需要改变的状态变量即可,不需要传入组件完整的 state

title 和 content 属性

this.state = {
    title: 'Jack',
    content: 'Welcome to React',
}

当只需要修改 title 属性时,只在 setState() 中修改 title 即可

this.setState({
    title: 'Tom',
});

浅合并之后的结果是

{
    title: 'Tom',
    content: 'Welcome to React',
}

2.4 ref

ref 调用方式:回调函数

class Demo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isInputShow: false  // 控制 input 是否渲染
        };
    }

    inputRefcb(instance) {
        if (instance) {
            instance.focus();
        }
    }

    render() {
        {
            this.state.isInputShow ?
            <div>
                <input ref={this.inputRefcb} type="text"/>
            </div>
            : null
        }
    }
}

触发回调函数的时机

ref 调用方式:字符串

class Demo extends Component {
    constructor(props) {
        super(props);
    }

    onfocus() {
        this.refs.inputRef.focus()
    }
    
    render() {
        {
            this.state.isInputShow ?
            <div>
                <input ref="inputRef" type="text"/>
                <input type="button" value="Focus" onClick={this.onfocus}/>
            </div>
            : null
        }
    }
}

父组件访问子组件的 DOM 节点

function TextInput(props) {
    return (
        <div>
            <input ref={props.inputRef} />
        </div>
    )
}

class Father extends Component {
    render() {
        return (
            <TextInput inputRef={
                e => this.inputElement = e
            } />
        );
    }
}

访问过程:

  1. 在父组件 Father 中引用子组件 TextInput
  2. 子组件 TextInput 通过 ref 传入 inputRef 函数
  3. 子组件 TextInput 又将这个回调函数作为 input 元素的 ref 属性
  4. 父组件 Father 可以通过 {this.inputElement} 获取子组件的 input 对应的 DOM 元素。

(三)React 高阶组件

3.1 定义与实现

编写一个高阶组件

  • 接受一个 WrappedComponent 组件
  • 返回一个 HOC 的 withHeader 组件
import React, {Component} from 'react';

export default function withHeader(WrappedComponent) {

    return class HOC extends Component {
        render() {
            return <div>
                <div className=" header">我是标题</div>
            </div>
        }
    }
}

高阶组件也可以作为一个普通组件使用

@withHeader
export default class Demo extends Component {
    render() {
        return (
            <div>我是一个普通组件</div>
        );
    }
}

@withHeader 是 ES7 中的装饰器语法,相当于下面的表达式

const EnhanceDemo = withHeader(Demo);

如果在某个组件中多次重复使用同一个高阶组件,在调试时就会看到一大堆相同的高阶组件,可以在使用时保留高阶组的原有名称来区分。

3.2 分类

高阶组件的实现方式:属性代理反向继承

const Container = (WrappedComponent) => class extends Components {
    render() {
        const newProps = {
            text: 'newText',
        }
        return <WrappedComponent> {...this.props} {...newProps} />
    }
}
const Container = (WrappedComponent) => class extends WrappedComponent {
    render() {
        return super.render();
    }

通过继承 WrappedComponent,可以使用 WrappedComponent 组件的 state、props、生命周期、render()等

3.3 命名与参数

displayName属性:当高阶组件包裹普通组件时,普通组件的名称和静态方法都会丢失,为了避免这种情况,给普通组件添加标组组件名称的 displayName属性

class HOC extends ...{
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
}

// getDisplayName 方法
function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName ||
            WrappedComponent.name ||
            'Component';
}

柯里化:通过传入不同的参数来得到不同的高阶组件

function HOCFactoryFactory(...params) {
    // 通过改变 params 来显示不同结果
    return class HOCFactory(WrappedComponent) {
        render() {
            return <WrappedComponent {...this.props} />
        }
    }
}

高阶组件的缺点

可能会造成静态方法丢失和 ref 属性不能传递,所以在使用过程中需要遵循一下准则

(四)组件通信

4.1 父子组件通信

父组件 --> 子组件

父组件通过 props 将值传递给子组件

class Parent extends Component {
    state = {
        params: 'father send msg to child'
    };
    render() {
        return <Child params={this.state.params} />;
    }
}
class Child extends Component { 
    render() {
        return <p>{this.props.params}</p>
    }
}

子组件 --> 父组件

  • 回调函数【最常见】
  • 自定义事件

回调函数方式:

  1. 父组件将一个函数作为 props 传递给子组件;
  2. 组组件调用该回调函数便可以向父组件传值;
class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            params: 'child send msg to father'
        };
    }
    
    transMsg(types) {
        console.log(type);
    }
    
    render() {
        return <Child params={this.state.params} />;
    }
}

class Child extends Component {
    constructor(props) {
        super(props);
        console.log("params :", this.props.params);
        this.props.transMsg("hi, fathre");
    }
    
    render() {
        return <p>{this.state.params}</p>
    }
}

4.2 跨级组件通信

父组件与子组件的子组件或者是更深层的子组件进行的通信。实现方式有两种:

使用组件 props 逐层传递 的缺点

使用 context 对象传递

代码

export default class GrandSon extends Component {
    // 子组件声明自己需要使用的 context
    static contextTypes = {
        color : PropTypes.string,
    };
    static propTypes = {
        value : PropTypes.string,
    };

    render() {
        const { value } = this.props;
        return (
            <li style={{background: this.context.color}}>
                <span>{value}</span>
            </li>
        );
    }
}

export default class Father extends Component {
    // 声明自己要使用的 context
    static fatherContextTypes = {
        color : PropTypes.string,
    };
    static propTypes = {
        name : PropTypes.string,
    };

    // 提供一个函数, 用来返回 context 对象
    getFatherContext() {
        return {
            color : 'red',
        };
    }
    render () {
        const {list} = this.props;
        return (
            <div>
                <ul>
                    {
                        list.map((entry, index) =>
                            <GrandSon key={`list-${index}`} value={entry.text} />
                        )
                    }
                </ul>
            </div>
        );
    }
}

class GrandFather extends Component {
    render() {
        return(
            <div>
                <Father name='GrandFather'/>
            </div>
        );
    }
}

如果组件中使用了构造函数,为了不影响跨级组件通信,还需要在构造函数中传入第二个参数 context

constructor(props, context) {
    super(props, context);
}

context 的缺点

因为context可以代表任何东西,所以它的类型是无法确定的,所以在使用的过程中也是需要谨慎对待

【总结】在父子组件通信模型中

其实如果将回调函数也看成一个属性,那么这两个过程其实都是一样的,都是子组件使用父组件提供的“属性”(变量或回调函数)

4.3 非嵌套组件通信

没有直接关系的两个组件,例如兄弟组件(同一个父节点下的两个节点)、完全不相干的两个组件。

对于兄弟组件,也是不可以直接通信的,可以通过状态提升来实现兄弟组件间的通信。提升状态就是值通过父组件进行中转,但是当层级较深时,中转过程也会特别复杂,如何寻找公共父组件也是一个问题。

自定义事件

安装 events 模块

通过自定义事件的方式来实现非嵌套组件间的通信,需要借助Node的events模块,通过以下命令安装 events 模块

npm install events --save

然后在 src 目录下创建一个 events.js 文件

import { EventEmitter } from 'events';
export default new EventEmitter();

再创建一个 ComponentA.js 文件

export default class ComponentA extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message : 'ComponentA',
        };
    }

    // 声明一个自定义事件
    componentDidMount() {
        this.eventEmitter = events.addListener('changeMessage', (message) => {
            this.setState ({
                message,
            });
        });
    }

    // 取消事件订阅
    componentWillUnmount() {
        events.removeListener(this.eventEmitter);
    }

    render() {
        return (
            <div>
                {this.state.message}
            </div>
        );
    }
}

再创建一个组件ComponentB

// 组件 ComponentB
export default class ComponentB extends Component {
    handleClick = (message) => {
        events.emit('changeMessage', message);
    };
    render() {
        return(
            <div>
                <button onClick={this.handleClick.bind(this, 'ComponentB')}>点击发送信息</button>
            </div>
        );
    }
}

创建测试用例,模拟两个非嵌套组件的通信

// 测试用例
export default class AppTest extends Component {
    render() {
        return (
            <div>
                <ComponentA />
                <ComponentB />
            </div>
        );
    }
}

【总结】原生通知了解下

(五)事件处理

5.1 事件监听与处理

React 事件 和 HTML 事件

为按钮添加一个事件

class demo extends Component {
    handleClick() {
        console.log('Click me')
    }

    render() {
        return(
            <button onClick={this.handleClick}>React 实战</button>
        );
    }
}

事件拦截

HTML 中通过 return false 来拦截事件

<a href="#" onclick="console.log('The link was clicked"); return false">
    Click me
</a>

React 使用虚拟DOM基础上实现的合成事件 SyntheicEvent

function ActionLink() {
    function handleClick(e) {
        e.prevenDefault();
    }

    return (
        <a href="#" onClick={handleClick}>Click me</a>
    );
}

5.2 event 事件与 this 关键字

event 事件

  1. React 在虚拟 DOM 的基础上实现的一套合成事件
  2. 处理监听时,需要传入一个 event 对象
  3. 完全符合 W3C 标准,所以可以完全兼容浏览器,并拥有和浏览器一样的事件接口

案例一:输出按钮的 innerHTML

class Demo extends Component {
    handleClick(e) {
        console.log(e.target.innerHTML)
    }

    render() {
        return(
            <button onClick={this.handleClick}>React 实战</button>
        );
    }
}

函数与对象方法

先来看一个例子,在上述方法中,如果输出 this,this结果是 null 或者 undefined

handleClick(e) {
        console.log(this)   // `null` 或者 `undefined`
    }

原因handleClick 是一个函数,并非是通过对象的方法调用的,而是直接的函数调用,所以在这个函数中,就无法获取到 this 所代表的类实例

解决办法:将函数绑定到当前实例上

render() {
        return(
            <button onClick={this.handleClick.bind(this)}>React 实战</button>
        );
    }

bind方法

  1. bind方式实现时间监听非常常见;
  2. bind是React在ES5引入的事件监听机制;
  3. bind格式:Function.prototype.bind()

bind原理

  1. 当调用函数对象的 bind() 方法时
  2. 系统会重新创建一个函数,新函数的行为和原函数一样
  3. 因为他们是由指定的 this 和初始化参数构造的原函数

bind传参

function f() {
    return this.a;
}
var g = f.bind({a:"azertyp"});
console.log(g());   //azertyp
var h = g.bind({a:"yoo"});  // bind 只生效一次
console.log(h());   //azertyp

5.3 EventEmitter 在 React Native 中的应用

EventEmitter 是用来处理原生和 React Native 之间通信的

iOS原生和 JavaScript 层交互关系表

原始端函数 JavaScript 层接口
sendEventWithName RCTAppEventEmitter
sendDeviceEventWithName RCTDeviceEventEmitter
sendInputEventWithName RCTInputEventEmitter

iOS 和 React Native 交互

iOS 中通过 eventDispatchersendAppEventWithName 方法将消息传递个 JavaScript

#import "CalendarManager.h"
#import "RCTEventDispatcher.h"

@implementation CalendarManager

@synchesize bridge=_bridge;

-(void)calendarEventReminderReceived:(NSNotification *)notification {
    NSString* eventName = notification.userInfo[@"name"];
    [self.bridge.eventDispatcher sendAppEventWithName"EventReminder" body:@{@"name" : eventName}];
}

@end

JavaScript 通过 addListener 订阅该事件,注意保持 name 一致,iOS中发出来的name是 EventReminder,所以addListener监听的也应该是 EventReminder

在事件使用完之后取消事件的订阅,即在 conponentWillUnmount 声明周期函数中取消事件的订阅。

import { NativeAppEventEmitter, NativeEventEmitter } from 'react-native';

var subscription = NativeEventEmitter.addListener(
    'EventReminder', (reminder) => console.log(reminder.name)
);
...
// 取消订阅事件
conponentWillUnmount() {
    subscription.remove();
}

Android 和 React Native 交互

Android中,通过 RCTDeviceEventEmitter 来注册事件

getReactApplicationContext()
    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
    .emit("EventReminder", null);

(六)React Hook

6.1 Hook 简介

  1. React Hook 是为了解决 React 的状态共享问题;
  2. 状态共享也可以看成是逻辑复用的问题;
  3. 因为 React Hook 治共享数据处理逻辑,并不会共享数据本身;

在 React 应用开发中,状态管理是组价你开发必不可少的内容。状态管理的方式:

  • 使用类组件
  • 使用 redux 等状态管理框架

案例【以前的做法】

class Example extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count : 0
        };
    }    

    render() {
        return (
            <div>
                <p>You clicked {this.state.count} times</p>
                <button onClick={()=>this.state({ count: this.state.count+1 })}>Click me</button>
            </div>
        );
    }
}

案例【现在的做法】

function Hook() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={()=>this.state({ count: this.state.count+1 })}>Click me</button>
        </div>
    );
}

6.2 Hook API

Hook API 背景故事

1. 如何解决状态组件的复用问题?

一般都是通过自上而下传递的数据流来将大型的视图拆分成独立的可复用组件。但是在实际开发中,如何复用一个带有业务逻辑的组件让让是一个问题。

2. 函数组件和类组件

前面介绍了他们的一些特性:函数组件缺少组件的 状态生命周期等特征,所以一直不受青睐

但是 Hool API 赋予了函数组件这些能力

React 提供了三个核心的 API

useState 组件

用来定义和管理本地状态

下面看一个计数器的小案例

function App() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <button onClick={ () => setCount(count+1)}>+</button>
            <span>{count}</span>
            <button onClick={ () => setCount(count-1)}>-</button>
        </div>
    )
}

export default App;

useState 的声明方式

const [count, setCount] = useState({
    count1: 0,
    coutn2: 0
});
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

在实际使用中,多次声明会更加方便,因为更新函数采用的是替换而不是合并。

如果要处理嵌套多层的数据逻辑,用 useState 就显得力不从心了,需要使用 React 提供的 useReducer 来处理这类问题

useReducer 的用法

import React, {useReducer} from 'react';

console reducer = function(state, action) {
    switch (action.type) {
        case "increment":   // 增加
            return {count: state.count+1};
        case "decrement":   // 减少
            return {count: state.count-1};
        default
            return {count: state.count};    
    }
};

function Example() {
    const [state, dispatch] = useReducer(reducer, {count:0});
    const {count} = state;
    return (
        <div>
            <button onClick={ () => dispatch({type: "increment"})}>+</button>
            <span>{count}</span>
            <button onClick={ () => dispatch({type: "decrement"})}>-</button>
        </div>
    );
}

export default Example;
function Example({initialState = 0}) {
    const {state, dispatch} = useReducer(reducer, { count: initialState} );
    ...
}

Effct Hook 管理声明周期

import React, {useState, useEffect} from 'react';


function Example() {
    const [count, setCount]= useState(0);

    useEffect( () => {
        console.log('componentDidMount...');
        console.log('componentDidUpdate...');
        return() => {
            console.log('componentWillUnmount...');
        }
    }
        
    );

    return (
        <div>
            <button onClick={ () => setCount(count+1)}>
                Click me
            </button>
        </div>
    );
}

export default Example;

每次点击按钮的时候,输出的内容为

componentDidMount...
componentDidUpdate...
componentWillUnmount...
componentDidMount...
componentDidUpdate...
...

6.3 自定义 Hook

  1. 自定义 Hook:函数名是以 use 开头的并调用其他 Hook 的封装函数
  2. 自定义 Hook 的每个状态都是独立的

使用 axios 实现一个自定义 Hook 的案例

import React, {useState, useEffect} from 'react';
import axios from 'axios';

export const useAxios = (url, dependecies) => {
    const [isLoading, setIsLoading] = useState(false);
    const [response, setReponse] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        setIsLoading(true);
        axios.get(Url).then((res) => {
            setIsLoading(false);
            setReponse(res);
        }).catch((err) => {
            setIsLoading(false);
            setError(err);
        });
    }, dependecies);

    return [isLoading, response, error];
};

在 Example 中使用 axois 自定义 Hook 函数组件

import React, {useState, useEffect} from 'react';
import axios from 'axios';

export const useAxios = (url, dependecies) => {
    const [isLoading, setIsLoading] = useState(false);
    const [response, setReponse] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        setIsLoading(true);
        axios.get(Url).then((res) => {
            setIsLoading(false);
            setReponse(res);
        }).catch((err) => {
            setIsLoading(false);
            setError(err);
        });
    }, dependecies);

    return [isLoading, response, error];
};

function Example() {
    let url = 'http://api.douban.com/v2/movie/in_theaters';
    const [isLoading, response, error] = useAxios(url, []);

    return (
        <div>
            {isLoading ? <div>Loading...</div> :
            (error ? <div> There is an error happned</div> : <div>Success {response}</div>
        }
        </div>
    );
}
 export default Example;

自定义 Hook 的优势

  1. 简洁易读
  2. 不会引起组件嵌套的问题

自定义 Hook 使用的注意事项

  1. 不要在循环、条件或嵌套函数中使用 Hook,并且只能在 React 函数的顶层使用 Hook,这是因为 React需要利用调用顺序来正确更新相应的状态,以及调用相应的生命周期函数。一旦在循环或条件分支语句中调用 Hook,就容易引起调用顺序不一致,产生难以预料的后果
  2. 只能在 React 函数式组件或自定义Hook中使用 Hook。

eslint

避免在开发中引起低级错误,可以在项目中安装一个 eslint 插件

yarn add eslint-plugin-react-hooks --dev

然后在eslint的配置文件中添加如下配置:

{
    "plugins" : [
        // ...
        "react-hooks"
    ],
    "rules" : [
        // ...
        "react-hooks/rules-of-hooks" : "error",
        "react-hooks/exhaustive-deps" : "warn",
    ]
}

借助 React 提供的 Hook API,函数组件可以实现绝大部分类组件功能,并且 Hook 在共享状态逻辑、提高组件复用性上也有一定的优势。可以预见的是,Hook将是 React 未来发展的重要方向。

上一篇 下一篇

猜你喜欢

热点阅读