Skip to content

React 原理

虚拟DOM

在React中,虚拟DOM的实质就是JavaScript对象

jsx代码

js
<div className='Index'>
  <div>我是小杜杜</div>
  <ul>
    <li>React</li>
    <li>Vue</li>
  </ul>
</div>

通过React.createElement将JSX代码存储为如下结构,这就是虚拟DOM:

json
{
    type: 'div',
    props: { class: 'Index' },
    children: [
        {
            type: 'div',
            children: '我是小杜杜'
        },
        {
            type: 'ul',
            children: [
                {
                    type: 'li',
                    children: 'React'
                },
                {
                    type: 'li',
                    children: 'Vue'
                },
            ]
        }
    ]
}

虚拟DOM的优势:

  • 提升开发效率

    使用虚拟DOM使得开发者更加关注于业务逻辑,而无需花费太多心思放在操作和更新原生JS DOM上。

  • 页面性能提升

    原生DOM操作会比较耗时。虚拟DOM在更新时会通过diff算法和批量处理策略比较来确定哪些部分需要更新,最终一次性去改变真实的DOM

    虽然依然避免不了操作真实DOM,但在绝大部分情况下(抛开首次渲染虚拟DOM会多了一层计算,有可能原生渲染的要慢)相比原生JS多次DOM操作的效率会提升很多。因为在操作真实DOM前会确定需要做的最小修改,尽可能的减少 DOM 操作带来的重流和重绘的影响

  • 超强的兼容性

    在浏览器方面,React基于实现了一套自己的事件机制,并且模拟了事件冒泡和捕获的过程,采取事件代理、批量更新等方法,从而磨平了各个浏览器的事件兼容性问题。

    跨平台方面,React和React Native都是根据虚拟DOM画出相应平台的UI层,只不过不同的平台画法不同而已

Diff算法

旧版Diff算法

React 16之前,React需要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构,另一棵在React状态变更将要重新渲染时生成。

Diff算法会对新旧两棵树做深度优先遍历(递归),避免对两棵树做完全比较,因此算法复杂度可以达到O(n)。然后给每个节点生成一个唯一的标志。

在遍历的过程中,每遍历到一个节点,就将新旧两棵树作比较,并且只对同一级别的元素进行比较, 然后记录下差异,更新真实DOM。

react_diff_1.png

Diff算法的具体策略:

Tree Diff

Tree diff主要针对的是React dom节点跨层级的操作

tree_diff.png

如图所示,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单地考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作

总结

基于上述原因,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真正地移除或添加 DOM 节点

Component Diff

Component Diff是专门针对更新前后的同一层级间的React组件比较的diff算法策略:

  • 如果是同一类型的组件,按照原策略继续比较 Virtual DOM 树(例如继续比较组件props和组件里的子节点及其属性)即可。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点,即销毁原组件,创建新组件。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切知道这点,那么就可以节省大量的 diff 运算时间。因此,React 允许用户通过shouldComponentUpdate来判断该组件是否需要进行 diff 算法分析

示例: component_diff.png

如图 所示,当组件 D 变为组件 G 时,即使这两个组件结构相似,一旦 React 判断 D 和G 是不同类型的组件,就不会比较二者的结构,而是直接删除组件 D,重新创建组件 G 及其子节点。

Element Diff

Element Diff是专门针对同一层级的所有节点(包括元素节点和组件节点)的diff算法。

当所有节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP(插入)MOVE_EXISTING(移动)REMOVE_NODE(删除)

  • 插入:新的节点不在旧集合对应顺序位置,需要对新节点执行插入操作。
  • 移动:节点存在于旧集合对应顺序位置且是可更新的类型(例如组件/节点类型一致,都是div),此时可复用之前的DOM节点,更新属性,执行移动操作。
  • 删除:原节点不在新的集合对应顺序位置,或者在新的集合中不能直接复用或更新(例如组件/节点类型不一致,由div变为span),对原节点执行删除操作。

element_diff.png

如图所示,旧集合中包含节点A、B、C 和 D,更新后的新集合中包含节点 B、A、D 和C。此时新旧集合按顺序进行逐一的diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除旧集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

React 发现这类操作烦琐冗余,因为这些都是相同的节点,但由于位置顺序发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可。

针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,见下面key机制。

新版Diff算法

React 16之后提出了Fiber机制和Fiber节点,对Diff算法机制进行了扩展优化:

不再是对虚拟DOM进行递归遍历比较差异,而是将原有的虚拟DOM节点转成Fiber节点,并拆分成一个个小的执行任务,找出所有节点的变更,如节点新增、删除、属性变更等信息。此外还增加了任务调度机制,允许React根据这些任务的紧急程度动态调整执行顺序,使得渲染过程可中断和恢复。最终再将这些任务的执行结果统一处理一次性更新真实DOM。

这种机制使得React能够更好地处理高优先级任务,如用户交互,同时不会阻塞其他重要操作的执行。

详见:React架构

Key机制

React可以给节点/组件(后续统称为元素)添加key props,来为当前元素指定唯一身份标识。这个属性是内置的,无法在该组件的内部通过props获取。

主要规则

React每次更新渲染时,对于属于同一层级的全体元素(同一层级只有一个元素也算),如果在新旧虚拟DOM树中:

规则

存在key相同的元素时:

  1. 如果key相同的元素的元素类型也相同(例如属于同一种组件或元素标签),则会初步判定为是同一元素,继续第2步。若元素类型不同则直接执行销毁-创建操作。

  2. 此时会判断新旧元素的顺序。如果新顺序比旧顺序靠后,则执行移动操作(即复用旧元素),并继续深入比较更新元素其他属性,否则不执行移动,同样继续深入比较更新元素其他属性。

不存在key相同的元素时:

则仍然执行Diff算法中的Element Diff策略.

示例1:

react_key_1.png

A,B,C,D四个元素在新旧集合中都含有各自key相同的元素,且元素类型都没发生改变。

A,C两个元素在新集合中相比旧集合各自的顺序都是靠后了,所以可以复用旧元素,执行移动操作移动至新的位置,然后继续深入内部更新。

B,D元素顺序相比旧顺序靠前,所以无需移动,然后继续深入内部比较更新。

示例2:

react_key_2.png

A,B,C元素在新旧集合中都含有各自key相同的元素,且元素类型都没发生改变。

A元素的新顺序相比旧顺序靠后,所以可以复用旧元素,执行移动操作即移动至新的位置,然后继续深入内部更新。

B元素的新顺序相比旧顺序靠前,C元素顺序不变。所以无需移动,然后继续深入内部比较更新。

D元素在新集合中没有发现key相同的元素,所以执行Element Diff策略,发现D元素已不存在,则执行删除操作。

E元素在旧集合中没有发现key相同的元素,所以执行Element Diff策略,发现D元素为新增节点,则执行创建操作。

示例3:

react_key_3.png

根据之前key的判断规则,A,B,C元素新顺序相比旧顺序靠后,所以可以复用旧元素,执行移动操作即移动至新的位置,然后继续深入内部更新。而D则无需移动。

但是我们可以发现,A、B、C元素之间仍然保持原有的顺序,理论上其实只需对D执行移动操作。所以这是key机制的一个缺点。

在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作。当节点数量过大或更新操作过于频繁时,这在一定程度上会影响 React 的渲染性能。

为什么列表元素渲染时不建议index作为同层级元素的key?

如果使用index作为列表元素key,当元素顺序发生变化时,根据上述key规则进行判断时,key值相同的新旧元素可能因为元素类型不同而导致了销毁-重建操作。此外即使元素类型相同,但是新旧元素仍然不是同一个元素/组件,容易发生状态错乱的bug(例如元素类型相同的A组件的内部状态覆盖到B组件上了)。

不过如果列表元素只是作为纯展示,而不涉及顺序变更,那使用index作为key还是没有问题的。

注意事项

  • key只是针对同一层级的节点进行了diff比较优化,而跨层级的节点互相之间的key值没有影响。
  • key值在比较之前都会被执行toString()操作,所以尽量不要使用object类型的值作为key,会导致同一层级出现key值相同的节点继而可能导致意料之外的业务bug。

总结

总体来看,其实只有在新旧集合中同一元素发生了顺序变化(包括插入和删除元素)时,这时使用Key才明显提升了渲染性能。如果新旧集合不存在元素顺序变化,是否使用了Key几乎没有区别。

MIT Licensed