美文网首页
02基础语法--005--React 技术详解

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

作者: 修_远 | 来源:发表于2020-09-03 23:46 被阅读0次
    目录

    (一)React 简介

    • 主要作用是为 MVC 模式中的视图(view)层构建界面视图
    • 还可以以插件的形式作用于 Web 应用程序的非视图部分,实现与其他 JavaScript 框架的整合

    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 组件基础知识

    组件定义

    • 组件是 React 的核心内容
    • 组件是视图页面的重要组成部分
    • 每一个视图页面都由一个或多个组件构成
    • 组件是 React 应用程序的基石

    组件分类:无状态组件

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

    • 无状态组件是最基本的组件存在形式
    • 构成:由 props属性 和 render渲染函数 构成
    • 好处:由于不涉及状态的更新,复用性最强

    组件分类:有状态组件

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

    • 生命周期:有状态组件有生命周期,在不同的时刻触发组件状态的更新
    • 用处:有状态组件被大量使用在业务逻辑开发中

    组件的创建

    • ES5 的 React.createClass 方式,逐渐被下面的 ES6 代替
    • ES6 的 React.Component 方式
    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>
    }
    

    【注意】

    • 无状态组件一般会搭配高阶组件(OHC)一起使用
    • 高阶组件主要用来托管 state
    • Redux 框架就是通过 store 来管理数据源和组件的所有状态,其中所有负责展示的组件都使用无状态函数式写法
    • 无状态组件被大规模应用在大型应用程序中
    • 缺点:无状态组件在被 React 调用之前,组件还没有实例化,所以它不支持 ref 特性

    2.2 props

    • props 是组件对外的接口,一般情况下,props是不变的
    • state 是组件对内的接口

    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;
    
    • 通过 {this.props.name} 方式获取 props 中的值
    • ES5的语法中,通过 getDefaultProps() 方法中设置默认值

    在子类中定义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>
            )
        }
    }
    
    • 如果父类需要向子组件传递数据,只需要在组件中引入子组件,然后使用组件提供的props属性,即可向子组件传递数据
    • 子组件 props 接受的数据格式由 PropTypes 进行检测

    2.3 state

    • props 是组件对外的接口,一般情况下,props是不变的,props对使用它的组件来说是只读的,如果要修改props,只能通过组件的父类组件修改
    • 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
    • content 保持原有状态

    浅合并之后的结果是

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

    2.4 ref

    • 本质就是调用 ReactDOM.render()返回的组件实例,用来表示对组件真正实例的引用。
    • 具体使用时,可以将它绑定到组件的 render() 上,然后就可以用它输出组件的实例。
    • 可以使用 ref 方式来修改子组件。
    • ref 不仅可以挂在到组件上,还可以作用于具体的 DOM 元素。挂载到 DOM 元素时,ref 可以表示具体的 DOM 元素节点。
    • ref 表示对组件实例的引用时,不能再函数式组件内上使用 ref 属性。
    • 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
            }
        }
    }
    

    触发回调函数的时机

    • 组件被渲染后,回调参数 instance 作为 input 的组件实例的引用,可以立即使用该组件
    • 组件被卸载后,回调参数 instance 此时为 null,这样可以确保内存不被泄露
    • ref 属性本身发生改变,原有的 ref 会再次被调用,此时的回调参数instance 变成具体的组件实例

    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
            }
        }
    }
    
    • 通过 this.refs.inputRef 来获取组件实例
    • 不能在函数式声明组件中使用 ref,因为他们不能获取组件的实例

    父组件访问子组件的 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 定义与实现

    • 高阶组件是接受 React组件 作为参数,并返回一个新的 React组件 的组件。
    • 高阶组件可以看做是对传入的 React组件 经过一系列处理,最后返回一个相对增强的 React组件
    • 高阶组件本质上一个函数,不是组件,可以参考链式编程

    编写一个高阶组件

    • 接受一个 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} />
        }
    }
    
    • 反向继承:通过返回继承原组件来控制 render 函数,进而实现高阶组件。相对于属性代理,反向继承能访问到的区域和权限更多
    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 属性不能传递,所以在使用过程中需要遵循一下准则

    • 不要在组件的 render() 中使用高阶组件,也尽量避免在组件的其他生命周期方法中使用高阶组件
    • 如果需要使用被包装组件的静态方法,那么必须手动赋值静态方法
    • ref 应避免传递给包装组件

    (四)组件通信

    4.1 父子组件通信

    父组件 --> 子组件

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

    • 父组件传值:params={this.state.params}
    class Parent extends Component {
        state = {
            params: 'father send msg to child'
        };
        render() {
            return <Child params={this.state.params} />;
        }
    }
    
    • 子组件接收值:{this.props.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 对象传递

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

    • 每一层都需要传递 props
    • 增加程序的复杂度
    • 实际开发中不建议使用

    使用 context 对象传递

    • 父组件需要声明支持 context,并提供一个函数来返回相应的 context 对象
    • 子组件声明需要使用的 context 对象,并提供需要使用的 context 属性的 PropTypes

    代码

    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 非嵌套组件通信

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

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

    自定义事件

    • 发布/订阅模型
    • 给事件对象上添加监听器
    • 通过触发事件来实现组件之间的通信
    • 在组件的 componentDidMount 中声明自定义事件
    • 在组件的 componentWillUnmount 中取消订阅事件

    安装 events 模块

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

    npm install events --save
    

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

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

    再创建一个 ComponentA.js 文件

    • 在组件ComponentA.jscomponentDidMount 中声明自定义事件
    • 在组件ComponentA.jscomponentWillUnmount 中取消订阅事件
    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

    • 添加一个点击事件
    • ComponentA 接收到 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>
            );
        }
    }
    

    【总结】原生通知了解下

    • 组件 ComponentA 监听通知
    • 组件 ComponentB 发送通知

    (五)事件处理

    5.1 事件监听与处理

    React 事件 和 HTML 事件

    • React 事件使用驼峰命名法,而非全部小写
    • React中,可以传递一个函数作为事件的处理函数,而非一个简单的字符串

    为按钮添加一个事件

    • 只需要给 React 元素添加 onClick、onKeyDown 函数即可
    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

    • React 的时间处理程序接受的是 SyntheicEvent实例
    • stopPrepagation():阻止时间传递,目的是不让事件分派到其他的 Document 节点,但是默认事件依然会执行
    • preventDefault():通知浏览器不要执行与事件关联的默认动作,但事件依然会继续传递。
    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传参

    • 格式:bind(this, arg1, arg2, ...)
    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 中使用 RCTEventEmitter
    • Android 中使用 RCTDeviceEventEmitter

    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>
            );
        }
    }
    

    案例【现在的做法】

    • 使用 React Hook 提供的 State Hook 来处理状态;
    • 针对已经存在的类组件,也可以使用 State Hook 很好的进行重构;
    • Example 变成了一个函数组件,有自己的状态,还可以更新自己的状态;
    • useState函数 是 React 的一个 hook 函数,它的作用是声明状态变量。
    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

    • State API:状态API
    • Effect API:声明周期API
    • Custom API:自定义API

    useState 组件

    用来定义和管理本地状态

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

    • 函数组件的对象也可以是基础类型值
    • useState 返回的是一个数组,数组的第一个对象表示当前状态的值,第二个对象表示用于更改状态的函数,类似于类组件的 setState
    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;
    
    • useReducer 接受 reducer 函数和默认值两个参数
    • 返回当前状态 state 和 dispatch 函数的数组
    • 使用方式与 Redux 框架一致
    • useReducer 没有采用 Redux 方式设置默认值,是因为React认为状态的默认值可能来自于函数组件的props
    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...
    ...
    
    • 每次执行组件更新时,useEffect 中的回到函数都会被调用;
    • 在重新绘制前执行销毁操作,避免造成内存泄漏;
    • useEffect 可以被视为 componentDidMount, componentDidUpdate, componentWillUnmount 的数组,并用它关联函数组件的生命周期
    • 类组件的 componentDidMount, componentDidUpdate 声明周期函数都是在 DOM 更新后同步执行的
    • useEffect 不是同步执行的,不会阻塞浏览器更新界面
    • 需要模拟生命周期的同步执行,可以使用 React 提供的 useLayoutEffect Hook

    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 未来发展的重要方向。

    相关文章

      网友评论

          本文标题:02基础语法--005--React 技术详解

          本文链接:https://www.haomeiwen.com/subject/woyasktx.html