上篇文章先容了首次渲染时,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>
- );
- }
- }
复制代码 结果:
会发现,新的子组件加到最后去了,同时会打印一次 子 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企服之家,中国第一个企服评测及商务社交产业平台。
|