聊一聊Diff算法(React、Vue2.x、Vue3.x)

3,649 阅读5分钟

场景

计算两颗树形结构差异并进行转换

文中, 所提及n均代表节点的个数


传统Diff算法

处理方案: 循环递归每一个节点

传统diff

如上所示, 左侧树a节点依次进行如下对比:

a->e、a->d、a->b、a->c、a->a

之后左侧树其它节点b、c、d、e亦是与右侧树每个节点对比, 算法复杂度能达到O(n^2)

查找完差异后还需计算最小转换方式,这其中的原理我没仔细去看,最终达到的算法复杂度是O(n^3)

将两颗树中所有的节点一一对比需要O(n²)的复杂度,在对比过程中发现旧节点在新的树中未找到,那么就需要把旧节点删除,删除一棵树的一个节点(找到一个合适的节点放到被删除的位置)的时间复杂度为O(n),同理添加新节点的复杂度也是O(n),合起来diff两个树的复杂度就是O(n³)


优化的Diff算法

vue和react的虚拟DOM的diff算法大致相同,其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
  2. 同一层级的一组节点,他们可以通过唯一的id进行区分

(优化的)diff三点策略:

  • web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
  • 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构。
  • 对于同一层级的一组自节点,他们可以通过唯一id进行区分。

即, 比较只会在同层级进行, 不会跨层级比较

React优化Diff算法

基于以上优化的diff三点策略,react分别进行以下算法优化

  • tree diff
  • component diff
  • element diff
tree diff

react对树的算法进行了分层比较。react 通过 updateDepth对Virtual Dom树进行层级控制,只会对相同颜色框内的节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在,则该节点和其子节点都会被删除。这样是需要遍历一次dom树,就完成了整个dom树的对比

分层比较 img

如果是跨层级的移动操作,如图

跨层级操作 img

当根结点发现A消失了,会删除掉A以及他的子节点。当发现D上多了一个A节点,会创建A(包括其子节点)节点作为子节点

所以:当进行跨层级的移动操作,react并不是简单的进行移动,而是进行了删除和创建的操作,这会影响到react性能。所以要尽量避免跨层级的操作。(例如:控制display来达到显示和隐藏,而不是真的添加和删除dom)

component diff
  • 如果是同类型的组件,则直接对比virtual Dom tree
  • 如果不是同类型的组件,会直接替换掉组件下的所有子组件
  • 如果类型相同,但是可能virtual DOM 没有变化,这种情况下我们可以使用shouldComponentUpdate() 来判断是否需要进行diff

component vs img

如果组件D和组件G,如果类型不同,但是结构类似。这种情况下,因为类型不同,所以react会删除D,创建G。所以我们可以使用shouldComponentUpdate()返回false不进行diff。

针对react15, 16出了新的生命周期

所以:component diff 主要是使用shouldComponentUpdate() 来进行优化

element diff

element diff 涉及三种操作:插入,移动,删除

不使用key的情况 img

不使用key的话,react对新老集合对比,发现新集合中B不等于老集合中的A,于是删除了A,创建了B,依此类推直到删除了老集合中的D,创建了C于新集合。=

酱紫会产生渲染性能瓶颈,于是react允许添加key进行区分

使用key的情况 img

react首先对新集合进行遍历,for( name in nextChildren),通过唯一key来判断老集合中是否存在相同的节点,如果没有的话创建,如果有的话,if (preChild === nextChild ) 进行移动操作

移动优化
在移动前,会将节点在新集合中的位置和在老集合中lastIndex进行比较,如果if (child._mountIndex < lastIndex) 进行移动操作,否则不进行移动操作。这是一种顺序移动优化。只有在新集合的位置 小于 在老集合中的位置  才进行移动。

如果遍历的过程中,发现在新集合中没有,但是在老集合中的节点,会进行删除操作

所以:element diff 通过唯一key 进行diff 优化。

总结:
1.react中尽量减少跨层级的操作。 2.可以使用shouldComponentUpdate() 来避免react重复渲染。 3.添加唯一key,减少不必要的重渲染

Vue优化Diff

vue2.0加入了virtual dom,和react拥有相同的 diff 优化原则

差异就在于, diff的过程就是调用patch函数,就像打补丁一样修改真实dom

  • patchVnode
  • updateChildren

updateChildren是vue diff的核心
过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较

Vue 2.x vs Vue 3.x

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅

Vue3.x借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看

该算法中还运用了动态规划的思想求解最长递归子序列


最后, 希望大家早日实现:成为前端高手的伟大梦想!!!

欢迎交流~

  • 微信公众号:连先生有猫病
  • 微信号:wanderlian
  • 掘金:曜灵
  • 个人博客:lianpf.github.io/