跳到主要内容

认识

2024年03月14日
柏拉文
越努力,越幸运

一、认识


Vue.js 2.0 采用的是 双端 Diff 算法,同时对新旧两组子节点的两个端点进行比较。首先对比新旧头节点, 寻找可复用节点, 接着对比新旧尾节点、对比旧头新尾、对比旧尾新头, 最后锁定中间乱序的部分。剩余旧节点构建以 oldVNode.keykey, oldVNode 索引为 valueMap 结构。根据 newVNode.key 得到旧节点索引, 如果有设置 key, 从 Map 结构中查找, 如果没有设置 key, 遍历剩余旧节点。得到旧节点索引, 如果没有旧节点索引, 说明是新增元素, 新增节即可, 如果有索引, 说明旧节点也存在, 此时判断是否可复用, 如果可复用, 打补丁并移动节点, 如果不可复用, 说明是新增元素, 新增节点。最后新增剩余新节点, 删除剩余旧节点。

双端Diff 的优势在于对于同样的更新场景, 执行的 DOM 移动操作次数更少

二、节点结构


2.1 旧节点

2.2 新节点

2.3 新旧节点

三、单节点细节


3.1 key 相同 type 相同

key 相同 type 相同, 复用当前旧节点

3.2 key 相同 type 不同

key 相同 type 不同, 不存在任何复用的可能性, 删除所有旧节点

3.3 key 不同 type 相同

key 不同 type 相同, 当前节点不可复用, 删除当前旧节点, 继续遍历

3.4 key 不同 type 不同

key 不同 type 不同, 不存在任何复用的可能性, 删除所有旧节点

四、多节点细节


Vue.js 2.0 中,当数据发生改变时, 订阅者 watcher 就会调用 patch 给真实的 DOM 打补丁。通过 isSameVnode 进行判断, 相同则调用 patchVnode 方法。

patchVNode 做了如下操作:

  1. 找到对应的真实 DOM, 称为 el

  2. 如果都有文本节点且不相等, 将 el 文本节点设置为 VNode 的文本节点

  3. 如果 oldVNode 有子节点而 VNode 没有, 则删除子节点

  4. 如果 oldVNode 没有子节点而 VNode 有, 则将 VNode 的子节点真实化后添加到 el

  5. 如果两者都有子节点, 则执行 updateChildren 函数比较子节点

updateChildrenVue.js 2.0 Diff 算法, 采用双端对比的方式, 新旧 VNOde Array 的头和尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。

4.1 oldStart 与 newStart

当新老 VNode 节点的 start 相同时, 直接 patchVNode, 同时新老 VNode 节点的开始索引都加 1

4.2 oldEnd 与 newEnd

当新老 VNode 节点的 end 相同时, 同样直接 patchVNode, 同时新老 VNode 节点的结束索引都减 1

4.3 oldStart 与 newEnd

当老 VNode 节点的 start 和新 VNode 节点的 end 相同时, 这时候在 patchVNode 后,还需要将当前真实 dom 节点移动到 oldEndVNode 的后面, 同时老 VNode 节点开始索引加1, 新VNode 节点的结束索引减1

4.4 oldEnd 与 newStart

当老 VNode 节点的 end 和新 VNode 节点的 start 相同时, 这时候在 patchVNode 后, 还需要将当前真实 dom 节点移动到 oldStartVNode 的前面, 同时老 VNode 节点结束索引减 1, 新 VNode 节点的开始索引加1

4.5 没有相同节点复用

如果都不满足以上四种情形, 那说明没有相同的节点可以复用, 则会分为以下两种情况:

  1. 从旧的 VNodekey 值, 对应 index 索引为 value 值的哈希表中找到与 newStartVNode 一致 key 的旧的 VNode 节点, 再进行 patchVNode, 同时将这个真实的 dom 移动到 oldStartVNode 对应的真实 dom 的前面

  2. 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

五、思考


5.1 Vue Diff 中 key 的作用?

React Diff Key 作为新旧元素的唯一标识, 当对比新旧元素时, 首先要对比的就是 key 是否相同。如果 key 不相同或者 key 不存在, 那么认为此时的旧元素不可复用, 直接将旧元素删除, 随后重新创建 Fiber 节点。

5.2 Vue Diff 为什么不能用随机数做 key?

通过随机数设定的 key, 则会产生无序性,可能会导致所有的 key 都匹配不上,然后舍弃掉之前所有构建出来的 fiber 节点,再重新创建新的节点。

5.3 Vue Diff 最好不要使用数组的下标做为 key ?

数组下标相对随机数来说,比较稳定一些。但数组下标对应的组件并不是一成不变的,只要在数组的前面或者中间插入元素时,该下标对应的元素就发生变化。虽然 key 没变,但对应的元素已经发生变化了。但是在 Diff 时, key 相同, type 相同会复用旧元素, 导致元素渲染错误。

因此, 数组下标作为 key 的场景是: 只有则初始时渲染一次,后续不再更新列表,只是对某个具体元素进行更新或事件的处理等, 或者没有新增、删除操作等的列表。

5.4 React Diff 为什么不采用双端对比来优化呢?

React Fiber 目前的结构如下:

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

React Diff 目前的对比方式是:

  • newChildren[i]oldFiber 对比

  • newChildren[i++]oldFiber.sibling 对比

从已上可以得知: Fiber 链表的数据结构的特点: 就是任何一个位置的 Fiber 节点,都可以非常容易知道它的父 Fiber, 第一个子元素的 Fiber,和它的兄弟节点 Fiber。却不容易知道它前一个 Fiber 节点是谁,这就是 React 中单向链表 Fiber 节点的特点。

React 不能通过双端对比进行 Diff 算法优化是因为目前 Fiber 上没有设置反向链表,而且想知道就目前这种方案能持续多久,如果目前这种模式不理想的话,那么也可以增加双端对比算法。

5.5 为什么 Vue 不需要使用 Fiber 或者 时间分片?

  1. 首先时间分片是为了解决 CPU 进行大量计算的问题,因为 React 本身架构的问题,在默认的情况下更新会进行过多的计算,就算使用 React 提供的性能优化 API,进行设置,也会因为开发者本身的问题,依然可能存在过多计算的问题。

  2. Vue 通过响应式依赖跟踪,在默认的情况下可以做到只进行组件树级别的更新计算,而默认下 React 是做不到的(据说 React 已经在进行这方面的优化工作了),再者 Vue 是通过 template 进行编译的,可以在编译的时候进行非常好的性能优化,比如对静态节点进行静态节点提升的优化处理,而通过 JSX 进行编译的 React 是做不到的。

  3. React 为了解决更新的时候进行过多计算的问题引入了时间分片,但同时又带来了额外的计算开销,就是任务协调的计算,虽然 React 也使用最小堆等的算法进行优化,但相对 Vue 还是多了额外的性能开销,因为 Vue 没有时间分片,所以没有这方面的性能担忧。

  4. 根据研究表明,人类的肉眼对 100 毫秒以内的时间并不敏感,所以时间分片只对于处理超过 100 毫秒以上的计算才有很好的收益,而 Vue 的更新计算是很少出现 100 毫秒以上的计算的,所以 Vue 引入时间分片的收益并不划算。

六、React vs Vue2.0 vs Vue3.0


6.1 相同点

  1. 在进行更新 Diff 对比的时候,都是优先处理简单的场景,再处理复杂的场景。

  2. 在处理第一轮循环剩余的节点,都需要把节点处理 key - valueMap 数据结构,方便在往后的比对中可以快速通过节点的 key 取到对应的节点。同样在比对两个新老节点是否相同时,key 是否相同也是非常重要的判断标准。所以无论是 React, 还是 Vue,在写动态列表的时候,都需要设置一个唯一值 key,这样在 diff 算法处理的时候性能才最大化。

6.2 不同点

  1. key - valueMap 结构:
  • React.js: 构建以 oldFiber.keykey, oldFibervalueMap 结构

  • Vue.js 2.0: 剩余旧节点构建以 oldVNode.keykey, oldVNode 索引为 valueMap 结构

  • Vue.js 3.0: 遍历剩余新节点, 构建以 newChild.keykey, 以当前遍历位置 ivalue剩余新节点映射表

  1. 处理逻辑: React 中是先处理左边部分,左边部分处理不了,再进行复杂部分的处理; Vue2 则先进行首尾、首首、尾尾部分的处理,然后再进行中间复杂部分的处理;Vue3 则先处理首尾部分,然后再处理中间复杂部分,Vue2Vue3 最大的区别就是在处理中间复杂部分使用了最长递增子序列算法找出稳定序列的部分。

  2. 对静态节点的处理不一样: 由于 Vue 是通过 template 模版进行编译的,所以在编译的时候可以很好对静态节点进行分析然后进行打补丁标记,然后在 Diff 的时候,Vue2 是判断如果是静态节点则跳过过循环对比,而 Vue3 则是把整个静态节点进行提升处理,Diff 的时候是不过进入循环的,所以 Vue3Vue2Diff 性能更高效。而 React 因为是通过 JSX 进行编译的,是无法进行静态节点分析的,所以 React 在对静态节点处理这一块是要逊色的。

  3. 对比之后的更新时机: Vue2Vue3 的比对和更新是同步进行的,这个跟 React15 是相同的,就是在比对的过程中,如果发现了那些节点需要移动或者更新或删除,是立即执行的,也就是 React 中常讲的不可中断的更新,如果比对量过大的话,就会造成卡顿,所以 React16 起就更改为了比对和更新是异步进行的,所以 React16 以后的 Diff 是可以中断,Diff 和任务调度都是在内存中进行的,所以即便中断了,用户也不会知道。

  4. 对比算法: Vue2Vue3 都使用了双端对比算法,而 ReactFiber 由于是单向链表的结构,所以在 React 不设置由右向左的链表之前,都无法实现双端对比

参考资料


为什么 React 的 Diff 算法不采用 Vue 的双端对比算法?