一篇文章带你了解不可思议的react diff

react

前言

在认识react diff之前,首先要了解一个概念,virtual dom ,虚拟DOM结构,它是一种编程概念,在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调。

而react diff 就是VD的加速器,帮助界面更好地渲染出来。

React 中最值得称道的部分莫过于 Virtual DOM 与 diff 的完美结合,特别是其高效的 diff 算法,让用户可以无需顾忌性能问题而”任性自由”的刷新页面,让开发者也可以无需关心 Virtual DOM 背后的运作原理,因为 React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染,因此 Virtual DOM 与 diff 是保证 React 性能口碑的幕后推手。

其实diff算法并不是react 首推,但是正是react将这一算法进行了优化,我们才有必要了解其内部的实现原理。

传统的diff

计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n3),其中 n 是树中节点的总数。O(n3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。

如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。

因此,想要将 diff 思想引入 Virtual DOM,就需要设计一种稳定高效的 diff 算法,而 React 做到了!

那么,React diff 到底是如何实现的呢?

react diff

传统 diff 算法的复杂度为 O(n3),显然这是无法满足性能要求的。React 通过制定大胆的策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题。

diff 策略

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

  • tree diff
  • component diff
  • element diff

tree diff

基于策略一,react对树结构进行了优化,即对树进行分层比较,两棵树只会对同一层的节点进行比较。

如上图,只会对颜色相同的节点进行比较,即同一个父节点下的所有子节点,如果发现这个节点不存在,则直接删除这个节点和它的子节点,不会进行进一步比较,这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

但是,如果出现了 DOM 节点跨层级的移动操作,React diff 会有怎样的表现呢?

A节点被移动到了D节点的下边,

可以直接告诉你的是:react diff给出的处理方法是,先创建A B C这三个节点,然后删除原来的A节点和它的子节点。

所以在跨层级的操作中,它并没有按我们想象中的直接移动,而是进行了添加再删除,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。

所以我们在开发过程中,在写css代码的时候,最好控制元素的隐藏/显示,而不是添加或删除。

component diff

React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

它采取的模式是:

  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此

    React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

    如上图,虽然两个component结构相似,但是一旦发现D 和 G是两个不同类型的组件,就不会比较两者的结构,直接删除D组件,重新创建G组件和他的子组件

    但这是一种极端因素,因为很少会碰到两个不同的组件结构相似的,所以造成的影响基本不大。

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)

MOVE_EXISTING (移动)

REMOVE_NODE (删除)

  • INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

  • MOVE_EXISTING,在老集合有新 component 类型,且 element

    是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。

  • REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

    老集合中的元素是A、B、C、D

    新集合中的元素是B、A、D、C

    此时进行diff差异化对比,发现B!=A, 所以就会在新集合创建B节点,然后在老集合中删除A节点,依此类推,逐步创建A D C 删除 B C D。

我们可以发现 其实新集合的元素并没有较老集合增加,只是顺序变了。

为了解决这个问题,react引入了key,给每个元素指定唯一key,根据key值来判断元素是否发生变化,由此性能也大大提高。

这样在A B C D四个元素上加上key值以后,新集合中并没有检测到新元素,然后我们就开始进行移动。

移动的算法就是:

在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,

lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置)。

if(prevChild._mountIndex < lastIndex)则进行移动,否则不进行移动并更新lastIndex值。

我们以上图为例:

  • B在老集合的索引为1,初始lastIndex = 0,此时ele._mountIndex

    >lastIndex,则B不移动,对lastIndex进行更新, lastIndex = Math.max(lastIndex,prevChild._mountIndex),lastIndex = 1

    prevChild._mountIndex是B在老集合的位置

  • 然后prevChild._mountIndex = nextIndex = 0,表明B在新集合插入到了0这个位置,然后nextIndex++,去判断下一个元素,A;

  • A 在老集合是0,0<lastIndex,所以A移动,A就移动到nextIndex的位置,就是1,然后prevChild._mountIndex = nextIndex = 1,更新lastIndex,还是1,然后nextIndex++,去判断下一个元素;

  • D的prevChild._mountIndex = 4 > lastIndex,所以不移动,更新lastIndex = 4,D的prevChild._mountIndex = nextIndex = 2,nextIndex++

  • C节点延续上边的步骤,完成移动操作

以上主要分析新老集合中存在相同节点但位置不同时,对节点进行位置移动的情况,如果新集合中有新加入的节点且老集合存在需要删除的节点,那么 React diff 又是如何对比运作的呢?

如上图,其他操作和上述步骤相同,我们直接看E,他是个新元素,则创建新节点 E;更新 lastIndex ,并将 E 的位置更新为新集合中的位置,nextIndex++进入下一个节点的判断。

注意,最后移动完以后还有一步操作: 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

当然,React diff 还是存在些许不足与待优化的地方,如下图所示,若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

有兴趣的同学可以考虑如何优化

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

总结

  • react diff 将复杂度从O(n3)优化到了O(n)
  • 采用分层比较的策略,
  • 通过设置key,来对element diff进行优化
  • 建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。

参考

https://zhuanlan.zhihu.com/p/20346379

以上是 一篇文章带你了解不可思议的react diff 的全部内容, 来源链接: utcz.com/z/382197.html

回到顶部