React@16.x(21)渲染流程-更新

上篇文章介绍了首次渲染时,React 做的事情。
这篇介绍下在是如何更新节点的。

1,更新的2种场景

  1. 重新调用 ReactDOM.render(),触发根节点更新。
  2. 调用类组件实例的 this.setState(),导致该实例所在的节点更新。

2,节点更新

第1种情况,直接进入根节点的对比 diff 更新

第2种情况,调用this.setState()的更新流程:

  1. 运行生命周期函数 static getDerivedStateFromProps;
  2. 运行生命周期函数 shouldComponentUpdate,如果返回 false则到此结束,终止流程
  3. 运行 render,得到一个新的节点,进入该节点的对比 diff 更新
  4. 将生命周期函数 getSnapshotBeforeUpdate加入执行队列,以待将来执行
  5. 将生命周期函数 componentDidUpdate加入执行队列,以待将来执行。

后续步骤

  1. 更新虚拟DOM树;
  2. 完成真实DOM更新;
  3. 依次调用执行队列中的 componentDidMount
  4. 依次调用执行队列中的 getSnapshotBeforeUpdate
  5. 依次调用执行队列中的 componentDidUpdate

注意,这里的 componentDidMount 指的是子组件的,但子组件也不一定会执行(重新挂载)。
另外,涉及到的生命周期函数的执行顺序时,注意父 render 执行完后遍历进入子组件,当子组件的所有生命周期函数执行后,才会跳出循环继续执行父的其他生命周期函数。

3,对比 diff 更新

整体流程:将运行 render 产生的新节点,对比旧虚拟DOM树中的节点,发现差异并完成更新。

问题:如何确定,对比旧虚拟DOM中的哪个节点?

3.1,React 的假设

React 为了提高对比效率,会做以下假设:

  1. 节点不会出现层级移动,这样可以直接在旧树中找到对应位置的节点进行对比。
  2. 不同的节点类型,会生成不同的结构。节点类型指 React 元素的 type 值。
  3. 多个兄弟节点,通过 key 做唯一标识,这样可以确定要对比的新节点。 如果没有 key,则按照顺序进行对比。

3.1.2,key

如果某个旧节点有 key 值,则它在更新时,会寻找相同层级中相同 key 的节点进行对比。

所以,key 值应该在一定范围内(一般为兄弟节点之间)保持唯一,并保持稳定

保持稳定:不能随意更改,比如通过随机数生成,更新后随机数发生变化找不到旧值。(有意为之需要每次都使用新节点的情况除外)

2.1,找到了对比的目标

2.1.1,节点类型一致

根据不同的节点类型,做不同的事情:

1,空节点

无事发生。

2,DOM节点

  1. 直接重用之前的真实DOM对象,
  2. 属性的变化会记录下来,以待将来统一进行更新(此时不会更新),
  3. 遍历该DOM节点的子节点,递归对比 diff 更新

3,文本节点

  1. 直接重用之前的真实DOM对象,
  2. 将新文本(nodeValue)的变化记录下来,以待将来统一进行更新。

4,组件节点

1,函数组件

重新调用函数得到新一个新节点对象,递归对比 diff 更新

2,类组件
  1. 重用之前的实例;
  2. 运行生命周期函数 static getDerivedStateFromProps
  3. 运行生命周期函数 shouldComponentUpdate,如果返回 false则到此结束,终止流程
  4. 运行 render,得到一个新的节点,进入该节点的递归对比 diff 更新
  5. 将生命周期函数 getSnapshotBeforeUpdate加入执行队列,以待将来执行
  6. 将生命周期函数 componentDidUpdate加入执行队列,以待将来执行。

5,数组节点

遍历数组,递归对比 diff 更新

2.1.2,节点类型不一致

卸载旧节点,使用新节点。

1,类组件节点

直接放弃,并运行生命周期函数 componentWillUnmount,再递归卸载子节点。

2,其他节点

直接放弃,如果该节点有子节点,递归卸载子节点。

2.2,没有找到对比的目标

有2种情况:

  • 新的DOM树中有节点被删除,则卸载多余的旧节点。
  • 新的DOM树中有节点添加,则创建新加入的节点。

4,举例

例1,组件节点类型不一致

更新时如果节点类型不一致,那所有的子节点全部卸载,重新更新
不管子节点的类型是否一致。所以如果是类组件,会重新挂载并运行 componentDidMount

下面的例子中,就是因为节点类型发生变化 div --> p,所以当点击按钮切换时,子组件 Child 会重新挂载(3个生命周期函数都会执行),并且 button 也不是同一个。

import React, { Component } from "react";

export default class App extends Component {
    state = {
        visible: false,
    };

    changeState = () => {
        this.setState({
            visible: !this.state.visible,
        });
    };
    render() {
        if (this.state.visible) {
            return (
                <div>
                    <Child />
                    <button onClick={this.changeState}>toggle</button>
                </div>
            );
        } else {
            return (
                <p>
                    <Child />
                    <button onClick={this.changeState}>toggle</button>
                </p>
            );
        }
    }
}

// 子组件
class Child extends Component {
    state = {};

    static getDerivedStateFromProps() {
        console.log("子 getDerived");
        return null;
    }

    componentDidMount() {
        console.log("子 didMount");
    }

    render() {
        console.log("子 render");
        return <span>子组件</span>;
    }
}

例2,子节点结构发生变化

根节点类型一致,子节点结构发生变化。

下面的例子中,节点对比是按照顺序的,参考上文提到的React的假设1和假设3。

所以,当点击出现 h1 元素的节点对比更新过程中,

  1. 对比组件根节点 div,类型一致重用之前的真实DOM对象,遍历子节点。
  2. 新节点 h1 会和原来这个位置的旧节点 button 对比,不一致则删除旧节点 button。
  3. 新节点 button 发现没有找到对比的目标,则没有其他操作。
  4. 通过新虚拟DOM树,完成真实DOM更新。
export default class App extends Component {
    state = {
        visible: false,
    };

    changeState = () => {
        this.setState({
            visible: !this.state.visible,
        });
    };
    render() {
        if (this.state.visible) {
            return (
                <div>
                    <h1>标题1</h1>
                    <button className="btn" onClick={this.changeState}>
                        toggle
                    </button>
                </div>
            );
        } else {
            return (
                <div>
                    <button className="btn" onClick={this.changeState}>
                        toggle
                    </button>
                </div>
            );
        }
    }
}

所以,一般需要改变DOM 结构时,为了提升效率,要么指定 key来直接告诉 React 要对比的旧节点,要么保证顺序和层级一致。

上面的例子可以更改如下,这也是空节点的作用之一

render() {
   return (
        <div className="parent">
            {this.state.visible && <h1>标题1</h1>}
            <button className="btn" onClick={this.changeState}>
                toggle
            </button>
        </div>
    );
}

// 或
render() {
   return (
        <div className="parent">
            {this.state.visible ? <h1>标题1</h1> : null}
            <button className="btn" onClick={this.changeState}>
                toggle
            </button>
        </div>
    );
}

例3,key 的作用

下面的例子,子组件是类组件,有自己的状态,也会更改状态。
父组件以数组的形式渲染多个子组件,同时会在数组头部插入新的子组件。

import React, { Component } from "react";

class Child extends Component {
    state = {
        num: 1,
    };

    componentDidMount() {
        console.log("子 didMount");
    }

    componentWillUnmount() {
        console.log("子组件卸载");
    }

    changeNum = () => {
        this.setState({
            num: this.state.num + 1,
        });
    };
    render() {
        return (
            <div>
                <span>数字:{this.state.num}</span>
                <button onClick={this.changeNum}>加一</button>
            </div>
        );
    }
}

export default class App extends Component {
    state = {
        arr: [<Child />, <Child />],
    };

    addArr = () => {
        this.setState({
            arr: [<Child />, ...this.state.arr],
        });
    };
    render() {
        return (
            <div className="parent">
                {this.state.arr}
                <button className="btn" onClick={this.addArr}>
                    添加
                </button>
            </div>
        );
    }
}

效果:
在这里插入图片描述

会发现,新的子组件加到最后去了,同时会打印一次 子 didMount,并且 componentWillUnmount 并没有执行。

原因:因为没有设置 key,所以在新旧节点对比时,发现第1个节点类型一致,于是重用了之前的实例。直到对比到最后一个发现没有找到对比目标,才会用新的节点来创建真实DOM。

另外,正因为是类组件节点,所以并不会像我们印象中数组没有指定 key 时,如果往数组的开头插入元素,会导致所有的数组元素重新渲染。

增加 key 调整:

export default class App extends Component {
    state = {
        arr: [<Child key={1} />, <Child key={2} />],
        nextId: 3,
    };

    addArr = () => {
        this.setState({
            arr: [<Child key={this.state.nextId} />, ...this.state.arr],
            nextId: this.state.nextId + 1,
        });
    };
    render() {
        return (
            <div className="parent">
                {this.state.arr}
                <button className="btn" onClick={this.addArr}>
                    添加
                </button>
            </div>
        );
    }
}

以上。

相关推荐

  1. React@16.x23)useEffect

    2024-06-08 08:38:03       12 阅读
  2. React@16.x26)useContext

    2024-06-08 08:38:03       9 阅读
  3. React@16.x27)useCallBack

    2024-06-08 08:38:03       8 阅读
  4. React@16.x28)useMemo

    2024-06-08 08:38:03       8 阅读
  5. React@16.x25)useReducer

    2024-06-08 08:38:03       11 阅读
  6. React@16.x24)自定义HOOK

    2024-06-08 08:38:03       10 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-06-08 08:38:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-06-08 08:38:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-06-08 08:38:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-06-08 08:38:03       18 阅读

热门阅读

  1. 速盾:服务器cdn加速超时如何解决?

    2024-06-08 08:38:03       8 阅读
  2. WDF驱动开发-PNP和电源管理(一)

    2024-06-08 08:38:03       11 阅读
  3. xmind父主题快捷键Ctrl+Enter

    2024-06-08 08:38:03       8 阅读
  4. 关于json文件的保存

    2024-06-08 08:38:03       8 阅读
  5. 本地打包.Tar上传到服务器,服务器解压缩

    2024-06-08 08:38:03       8 阅读
  6. Hudi CLI 安装配置总结

    2024-06-08 08:38:03       6 阅读
  7. Go每日一库之rotatelogs

    2024-06-08 08:38:03       8 阅读
  8. python字典

    2024-06-08 08:38:03       9 阅读
  9. HTTPS和TCP

    2024-06-08 08:38:03       8 阅读