React 学习之渲染过程

971 阅读5分钟

渲染原理

术语描述:

渲染: 生成用于显示的对象,以及将这些对象形成真实的 DOM 对象

  1. React 元素 (React Element):通过 React.createElement 创建 (语法糖:JSX 表达式创建);如: <div>React Element</div><App />

  2. React 节点 (React Component):专门用于渲染到 UI 界面的对象 (Virtual DOM),React 会通过 React 元素 创建 React 节点,ReactDOM 一定是根据 React 节点来渲染页面的

    节点类型:

    • React DOM 节点 (Virtual DOM Node):创建该节点的 React 与元素类型是一个 字符串 ("div", "h1'等)
    • React 组件节点 (React ComponentNode):创建该节点的 React 与元素类型是一个 函数 或一个 (比如我们封装的函数组件或类组件等)
    • React 文本节点 (React TextNode):由字符串、数字创建
    • React 空节点 (React EmptyNode)null, undefined, false, true
    • React 数组节点 (React ArrayNode):由数组创建

首次渲染 (新节点挂载阶段)

  1. 通过参数的值创建节点 ReactDOM.render(参数, MONT_NODE)

  2. 根据不同的节点,做不同的事情

    • 文本节点:通过 document.createTextNode() 创建真实的文本节点
    • 空节点:不创建真实 DOM
    • 数组节点:遍历数组,对每一项递归进行创建节点 (回第 ① 步,直到遍历结束)
    • DOM 节点:JSX 解析生成的对象 (React 元素),通过 document.createElement() 创建真实 DOM 对象,然后遍历对应 React 元素的 children 属性(对象或数组),递归操作 (回第1步,直到遍历结束)
    • 组件节点函数组件:调用函数 (该函数必须返回可以生成节点的内容),将该函数的返回结果递归生成节点 (同上);类组件:创建该类的实例,立即调用对象的生命周期方法 static getDerivedStateFromProps,运行该对象的 render 方法,得到节点对象,递归操作;将该组件的 componentDidMount 加入到执行队列 (先进先执行),当整个虚拟 DOM 树全部构建完毕,并且将真实的 DOM 对象加入到容器中后,遍历该队列执行
  3. 生成虚拟 DOM 树后,将该树保存起来,以便后续使用 (diff)

  4. 将之前生成的真实 DOM 对象,加入到页面的容器中

更新节点

节点更新时机:

  1. 重新调用 ReactDOM.render() 方法,完全重新生成节点树 (虚拟 DOM 树),触发根节点的更新

  2. 在类组件中调用 this.setState() 更新状态,会导致该实例所在的节点更新

  3. hook 涉及的更新后续再说~

对比更新

  1. 如果调用 ReactDOM.render() 方法,对比更新根节点(diff)

  2. 如果调用 this.setState() 方法

    • 运行生命周期函数 static getDerivedStateFromProps
    • 运行生命周期函数 shouldComponentUpdate,若 返回 false,终止当前流程
    • 运行 render,得到一个新的节点,进入该新节点的 对比更新
    • 将生命周期函数 getSnapshotBeforeUpdate 加入执行队列,等待执行
    • 将生命周期函数 componentDidUpdate 加入执行队列 (与上面这步不是同一个队列),等待执行

对比更新的后续步骤:

  • 更新虚拟 DOM 树
  • 完成真实 DOM 的更新
  • 依次调用执行队列中的 compoentDidMount (产生的新组件挂载)
  • 依次调用执行队列中的 getSnapshotBeforeUpdate
  • 依次调用执行队列中的 componentDidUpdate
  • 依次调用执行队列中的 componentWillUnmount (被移除的子节点才会推入队列)

对比更新:将新产生的节点,与之前虚拟 DOM 中的节点进行对比,发现差异,进行更新

对比假设

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

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

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

    • 相同的节点类型:节点本身类型相同 (如文本类型,DOM节点);如果是组件节点,组件类型也得相同 (类 / 函数);若是由 React 元素生成,type 值必须是一致的
  3. 多个兄弟节点通过唯一标识 (key) 来确定对比的新节点

    • key 值用于通过旧节点来寻找应该对比的新节点,如果某个旧节点有 key 值,则其更新时,会寻找相同层级中具有相同 key 值的节点 (若未找到,则进入未找到对比节点的流程)

找到对比节点

判断节点类型是否一致

  1. 一致:根据不同的节点做不同的事:

    • 空节点:不做任何事;
    • DOM 节点:重用之前生成的真实 DOM 对象,将其属性的变化进行记录 (此时不会更新 DOM),遍历该新的 React 元素的子元素,递归对比更新;
    • 文本节点:直接重用之前的真实 DOM 对象,记录变化的 nodeValue 值;
    • 函数组件节点:重新调用函数,得到一个节点对象,进入递归对比更新;
    • 类组件节点:重用之前的实例,依次调用 static getDerivedStateFromProps, shouldComponentUpdate (若返回false,终止对比),否则运行 render,进入递归对比更新,将该对象的 getSnapshotBeforeUpdate, componentDidUpdate 加入相应的队列,等待执行;
    • 数组节点:遍历数组,进行递归对比更新
  2. 不一致:整体上,卸载旧的节点,重新创建新的节点 (先创建新节点,后卸载旧节点)。旧节点:

    • 空节点, 文本节点, DOM 节点, 数组节点, 函数组件节点:直接舍弃旧节点,创建新节点 (进入 新节点挂载阶段);
    • 类组件节点:直接舍弃旧节点,调用该节点的 componentWillUnmount 函数,递归卸载子节点

未找到对比目标

新的虚拟 DOM 树中有 新的节点删除或添加,即:创建新加入的节点,卸载多余的节点 (还是上面挂载跟卸载的流程)