1.新节点的挂载

1.1 React的基本术语

渲染:生成显示的对象,以及将这些对象形成真实的 DOM 对象的过程就叫做渲染。

React 元素(React Element): 通过 React.createElement 创建,也可以通过 JSX 表达式(语法糖) 创建,本质上是一个对象,并不是真实 DOM 元素。

import React from 'react';
import ReactDOM from 'react-dom';

const Test = <div><h1>标题</h1></div>
console.log(Test)
/* {
    "type": "div",
    "key": null,
    "ref": null,
    "props": {
        "children": {
            "type": "h1",
            "key": null,
            "ref": null,
            "props": {
                "children": "标题"
            },
            "_owner": null,
            "_store": {}
        }
    },
    "_owner": null,
    "_store": {}
} */
ReactDOM.render(Test, document.getElementById('root'));

React 元素:

<div><h1>1111</h1>
<App />

React 节点: 专门用于渲染到 UI 界面的对象,在源码里这些对象是私有的类,外部无法访问。React 会通过 React 元素,创建 React 节点,且 ReactDOM 一定是通过 React 节点来进行渲染的。以下是 React 中的几种节点类型:

  • React DOM 节点(源码里是 ReactDOMComponent):创建该节点的 React 元素类型(type)是一个字符串;

  • React 组件节点:创建该节点的 React 元素类型是一个函数或一个类;

  • React TextNode 节点:由字符串或数字创建的节点

    import React from 'react';
      import ReactDOM from 'react-dom';
    
      const str = "Hell World";
      ReactDOM.render(str, document.getElementById('root'));
  • React 空节点:由 null 、undefined 、false 、true 创建的节点

  • React 数组节点:该节点由一个数组创建的节点

React 元素创建 React 节点流程

1.1 React 元素创建 React 节点流程

真实 DOM:通过 document.createElement 创建的 DOM 元素

1.2 首次渲染(新节点渲染)

(1) 通过参数的值创建节点

(2) 根据不同的节点,做不通的事情:

  • 文本节点:通过 document.createTextNode(string) 创建真实的文本节点

  • 空节点:什么都不做(但该节点会占位,且节点对象是存在的)

  • 数组节点:遍历数组,将数组的每一项递归创建节点(回到第1步进行反复操作,直到遍历结束)

  • DOM 节点:通过 document.createElement() 创建真实的 DOM 对象(只是创建,并不会加入到页面中),然后立即设置该真实 DOM 元素的各种属性,遍历对应 React 元素的 children 熟悉,递归操作(回到第1步进行反复操作,直到遍历结束)

  • 组件节点:

    • 函数组件:调用函数,该函数必须返回一个可以生成节点的内容,将该函数的返回结果递归生成节点,直到遍历结束

    • 类组件:

      • 创建该类的实例对象

      • 立即调用对象的生命周期方法 static getDerivedStateFromProps

      • 运行该对象的 render 方法,拿到节点对象(将该节点递归操作,回到第1步『创建该类的实例对象』进行反复操作,直到遍历结束)

      • 将该组件的 componentDidMount 加入到执行队列(先进先出,先进先执行),当整个虚拟 DOM 树全部构建完毕,并将真实的 DOM 对象加入到容器中后,执行该队列

import React from 'react';
  import ReactDOM from 'react-dom';

  class Comp1 extends React.Component {
      state = {};
      constructor(props) {
          super(props);
          console.log(4, 'Comp1 Constructor');
      }

      static getDerivedStateFromProps(props, state) {
          console.log(5, 'Comp1 getDerivedStateFromProps');
          return null;
      }

      componentDidMount() {
          console.log('b', 'App componentDidMount');
      }

      render() {
          console.log(6, 'Comp1 render');
          return <h1>Comp1</h1>;
      }
  }

  export default class App extends React.Component {
      state = {};
      constructor(props) {
          super(props);
          console.log(1, 'App Constructor');
      }

      static getDerivedStateFromProps(props, state) {
          console.log(2, 'App getDerivedStateFromProps');
          return null;
      }

      componentDidMount() {
          console.log('a', 'App componentDidMount');
      }

      render() {
          console.log(3, 'App render');
          return (
              <div>
                  <Comp1 />
              </div>
          );
      }
  }

  ReactDOM.render(app, document.getElementById('root'));

  // 运行过程:1,2,3,4,5,6,b,a

(3) 生成出虚拟 DOM 树之后,将该树保存起来,以便后续使用

(4) 将之前生成的虚拟 DOM 树对象,加入到容器中

import React from 'react';
import ReactDOM from 'react-dom';

const app = (
    <div className="aaa">
        <h1>
            标题 {["abc", null, <p>段落</p>]}
        </h1>
        <p>{undefined}</p>
    </div>
);
ReactDOM.render(app, document.getElementById('root'));

以上代码生成的虚拟 DOM 树

1.2 以上代码生成的虚拟 DOM 树

2.更新节点

2.1 更新的场景

  • 重新调用 ReactDOM.render,完全重新生成节点树,触发根节点更新
  • 在类组件的实例对象中调用 setState,会导致该实例所在的节点更新

2.2 节点的更新

  • 如果调用的是ReactDOM.render,进入根节点的对比(diff)更新

  • 如果调用的是setState

    • a. 运行生命周期函数,static getDerivedStateFromProps

    • b. 运行 shouldComponentUpdate,如果该函数返回false,终止当前流程

    • c. 运行 render,得到一个新的节点,进入该新的节点的对比更新

    • d. 将生命周期函数 getSnapshotBeforeUpdate 加入执行队列,以待将来执行

    • e. 将生命周期函数 componentDidUpdate 加入执行队列,以待将来执行

    • 后续步骤:

      • 更新虚拟 DOM 树
      • 完成真实的 DOM 更新
      • 依次调用执行队列中的 componentDidMount
      • 依次调用执行队列中的 getSnapshotBeforeUpdate
      • 依次调用执行队列中的 componentDidUpdate
      • 依次调用执行队列中的 componentWillUnMout

2.3 对比更新

将新产生的节点,对比之前虚拟 DOM 中的节点,发现差异,完成更新。

Q: 对比之前 DOM 树中哪个节点?

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

  • 1.假设节点不会出现层级的移动(对比时,直接找到旧树中对应位置的节点进行对比)

  • 2.不同的节点类型会生成不同的结构

    • 相同的节点类型:节点本身类型相同,如果是由 React 元素生成,type 值需要保证一致
    • 其他的,都属于不同的节点类型
  • 3.多个兄弟节点通过唯一标识(key)来确定对比的新节点

    • key 值的作用:用于通过旧节点,寻找对应的新节点,如果某个旧节点有 key 值,则其更新时,会寻找相同层级中的相同 key 值的节点,进行对比。
    • key值应该在一个范围内唯一(兄弟节点中),并且应该保持稳定

2.3.1 找到了对比的目标

判断节点的类型是否一致:

1 一致,根据不同的节点类型,做不同的事情

  • 空节点,不做任何事情

  • DOM 节点:

    • 直接重用之前的真实 DOM 对象
    • 将其属性的变化记录下来,以待将来统一完成更新(此刻并不会真正的变化)
    • 遍历该新的 React 元素的子元素,递归对比更新
  • 文本节点:

    • 直接重用之前的真实 DOM 对象
    • 将新的文本变化记录下来,将来统一完成更新
  • 组件节点:

    • 函数组件:重新调用函数,得到一个节点对象,进入递归对比更新

    • 类组件:

      • 重用之前的实例
      • 调用生命周期方法 static getDerivedStateFromProps
      • 调用生命周期方法 shouldComponentDidUpdate,若该方法返回 false,终止
      • 运行 render,得到新的节点对象,进入递归对比更新
      • 将该对象的 getSnapshotBeforeUpate 加入队列
      • 将该对象的 componentDidUpate 加入队列
  • 数组节点: 遍历数组进行递归对比更新

2 不一致,整体上卸载旧的节点,全新创建新的节点

创建新节点:进入新节点的挂载流程

卸载旧节点:

  • 文本节点、DOM 节点、数组节点、空节点、函数组件节点:直接放弃该节点,如果有子节点,递归卸载子节点

  • 类组件节点:

    • 直接放弃该节点
    • 调用该节点的 componentWillUnMount 周期方法
    • 递归卸载子节点
// Bad button的 DOM 类型发生变化
render() {
    return this.state.visible ? ( <div>
            {h1}
        <button>按钮</button>
    </div>) : ( <div>
        <button>按钮</button>
    </div>)
}

// Good 尽量不要修改节点类型和节点结构
const h1 = this.state.visible ? <h1> 标题 </h1> : null
render() {
    return (<div>
            {h1}
        <button>按钮</button>
    </div>)
}

// Good 尽量不要修改节点类型和节点结构
render() {
    return(<div>
        <h1 style={{display: this.state.visible ? "block" : "none"}}> 标题 </h1>
         <button>按钮</button>
    </div>
    )
}

2.3.2 未找到对比的目标

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