世纪审案:React & Vue Diff 算法

74 阅读3分钟

前置知识

为什么要虚拟DOM

  • 用对象代替DOM,提高开发效率,提供抽象能力
  • 提供 JS 操作 DOM 的渲染层,抹平平台差异

为什么要使用 diff

  1. 对于树的比较是O(n^3)的,时间复杂度太高了,使用diff算法以优化Diff算法的复杂度
  2. React 通过只比较同层节点将时间复杂度降到 O(n) (业务中很少去跨层移动节点)

Diff 的基本流程

通过自上而下自左而右的遍历对象,(树的深度深度遍历),给每一个节点打上标签

当一个节点有子节点的时候,通过当前节点对新旧子节点做 Diff ,同层比较

Vue Diff

先判断子节点的情况

  1. 新旧节点都是静态的,同时key相同。不做diff

  2. 老节点没有children而新节点有,清空老节点DIff,挂载子节点

  3. 新节点没有children而老节点有,直接移除DOM子节点即可

  4. 新老节点都没有子节点,直接替换文本

  5. 新老节点都有children,对子节点进行 diff ,调用 updateChildren

Vue2 & Vue3 Diff 对比

vue2

双端对比算法

  1. 首首
  2. 尾尾
  3. 首尾
  4. 尾首

四次对比找到能够复用的节点,递归执行 patchVnode

vue3

  • 编译时静态标记,跳过静态节点比较

  • 将动态节点划分区块,只处理动态区块

  • 双指针 + 最长递增子序列做法

    • 头头
    • 尾尾
    • 中间最长递增子序列

只移动头指针和尾指针之间的,最长递增子序列之外的内容

Vue3 & React Diff 对比

React 16 +

  1. 对于fiber树,每次更新都是全量的,对于触发修改的修改的子树,更新也是全量 的

    1. 当一个组件发生了变化,他的子组件也会更新,不论porps是否改变。除非手动通过 react.memo 等手段优化。但是子组件 fiber 可能会更新,最后会因为props 没有发生变化而跳过更新到DOM
    2. 每次都是全量遍历 fiber tree 收集副作用函数
  2. fiber 树实际上是一个单向链表

  3. 因此 react diff 是单向两次遍历

  4. 强依赖 Key

  5. 利用 fiber + messageChannel 实现可中断的渲染

  6. 一直存在两棵树

  7. 优化依赖手动配置和时间切片

vue3

  1. 对于vnode树,生成和遍历是全量的(BFS)。但是对于有对于依赖追踪的局部更新

    1. 通过 Proxy 收集依赖,当数据变化的时候仅仅更新相关的组件 vnode 渲染和 dom 更新。
    2. 虽然每次都是全量生成vnode,但是vue通过静态标记和动态分区,静态节点提升等方式限制了 diff 的范围
  2. vnode 树实际上是一个双向链表,借鉴 snabbdom

  3. vue3 diff 是前后双指针+ LIS 完成的,效率更高

  4. 没有 key 也可以 diff

  5. 即使渲染可以批处理,但是每一次渲染都是同步的

  6. 只有在diff的时候才会存在两棵树

  7. 编译时动态标记静态和动态分区优化

React 和 Vue 在 diff 上的不同,其实是他们的设计哲学的延伸。

对于React,注重灵活性和控制力。强调显示的控制和可预测性。【开箱即用的高效】

对于Vue,注重渐进式和易用性。最小化开发者的心智负担。【显示控制的可预测性】

vue & snabbdom 对比

vue3 的 diff 算法其实就是 snabbdom 的优化

  • 使用编译时标记(静态提升/动态标记),对 diff 范围进行剪枝 ( 这是属性纬度的 )
  • 使用区块树进行分区(动态分区/静态分区) ,缩减 diff 范围 ( 这是 vnode 纬度的 )
  • 利用 proxy 追踪响应式数据, 进一步缩减 diff 范围

笔者才疏学浅,各位读者多多担待,不吝赐教。