农妇山泉一亩田 发表于 2024-6-21 13:15:21

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

上篇文章先容了首次渲染时,React 做的事变。
这篇先容下在是如何更新节点的。
1,更新的2种场景


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

第1种情况,直接进入根节点的对比 diff 更新。
第2种情况,调用this.setState()的更新流程:

[*]运行生命周期函数 static getDerivedStateFromProps;
[*]运行生命周期函数 shouldComponentUpdate,如果返回 false,则到此竣事,终止流程。
[*]运行 render,得到一个新的节点,进入该节点的对比 diff 更新。
[*]将生命周期函数 getSnapshotBeforeUpdate加入执行队列,以待将来执行
[*]将生命周期函数 componentDidUpdate加入执行队列,以待将来执行。
后续步骤:

[*]更新假造DOM树;
[*]完成真实DOM更新;
[*]依次调用执行队列中的 componentDidMount
[*]依次调用执行队列中的 getSnapshotBeforeUpdate
[*]依次调用执行队列中的 componentDidUpdate
   注意,这里的 componentDidMount 指的是子组件的,但子组件也不肯定会执行(重新挂载)。
别的,涉及到的生命周期函数的执行顺序时,注意父 render 执行完后遍历进入子组件,当子组件的全部生命周期函数执行后,才会跳出循环继承执行父的其他生命周期函数。
3,对比 diff 更新

整体流程:将运行 render 产生的新节点,对比旧假造DOM树中的节点,发现差别并完成更新。
题目:如何确定,对比旧假造DOM中的哪个节点?
3.1,React 的假设

React 为了进步对比效率,会做以下假设:

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

如果某个旧节点有 key 值,则它在更新时,会寻找雷同层级中雷同 key 的节点举行对比。
所以,key 值应该在肯定范围内(一样平常为兄弟节点之间)保持唯一,并保持稳定。
   保持稳定:不能随意更改,比如通过随机数天生,更新后随机数发生厘革找不到旧值。(有意为之须要每次都使用新节点的情况除外)
2.1,找到了对比的目标

2.1.1,节点类型一致

根据不同的节点类型,做不同的事变:
1,空节点

无事发生。
2,DOM节点


[*]直接重用之前的真实DOM对象,
[*]属性的厘革会记载下来,以待将来同一举行更新(此时不会更新),
[*]遍历该DOM节点的子节点,递归对比 diff 更新。
3,文本节点


[*]直接重用之前的真实DOM对象,
[*]将新文本(nodeValue)的厘革记载下来,以待将来同一举行更新。
4,组件节点

1,函数组件

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


[*]重用之前的实例;
[*]运行生命周期函数 static getDerivedStateFromProps;
[*]运行生命周期函数 shouldComponentUpdate,如果返回 false,则到此竣事,终止流程。
[*]运行 render,得到一个新的节点,进入该节点的递归对比 diff 更新。
[*]将生命周期函数 getSnapshotBeforeUpdate加入执行队列,以待将来执行
[*]将生命周期函数 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 元素的节点对比更新过程中,

[*]对比组件根节点 div,类型一致重用之前的真实DOM对象,遍历子节点。
[*]新节点 h1 会和原来这个位置的旧节点 button 对比,不一致则删除旧节点 button。
[*]新节点 button 发现没有找到对比的目标,则没有其他操纵。
[*]通过新假造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>
      );
    }
}
结果:
https://img-blog.csdnimg.cn/direct/278069be887648f596bedd124b1a4280.gif#pic_center
会发现,新的子组件加到最后去了,同时会打印一次 子 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>
      );
    }
}
以上。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: React@16.x(21)渲染流程-更新