前置知识
为什么要虚拟DOM
- 用对象代替DOM,提高开发效率,提供抽象能力
- 提供 JS 操作 DOM 的渲染层,抹平平台差异
为什么要使用 diff
- 对于树的比较是O(n^3)的,时间复杂度太高了,使用diff算法以优化Diff算法的复杂度
- React 通过只比较同层节点将时间复杂度降到 O(n) (业务中很少去跨层移动节点)
Diff 的基本流程
通过自上而下自左而右的遍历对象,(树的深度深度遍历),给每一个节点打上标签
当一个节点有子节点的时候,通过当前节点对新旧子节点做 Diff ,同层比较
Vue Diff
先判断子节点的情况
-
新旧节点都是静态的,同时key相同。不做diff
-
老节点没有children而新节点有,清空老节点DIff,挂载子节点
-
新节点没有children而老节点有,直接移除DOM子节点即可
-
新老节点都没有子节点,直接替换文本
-
新老节点都有children,对子节点进行 diff ,调用 updateChildren
Vue2 & Vue3 Diff 对比
vue2
双端对比算法
- 首首
- 尾尾
- 首尾
- 尾首
四次对比找到能够复用的节点,递归执行 patchVnode
vue3
-
编译时静态标记,跳过静态节点比较
-
将动态节点划分区块,只处理动态区块
-
双指针 + 最长递增子序列做法
- 头头
- 尾尾
- 中间最长递增子序列
只移动头指针和尾指针之间的,最长递增子序列之外的内容
Vue3 & React Diff 对比
React 16 +
-
对于fiber树,每次更新都是全量的,对于触发修改的修改的子树,更新也是全量 的
- 当一个组件发生了变化,他的子组件也会更新,不论porps是否改变。除非手动通过 react.memo 等手段优化。但是子组件 fiber 可能会更新,最后会因为props 没有发生变化而跳过更新到DOM
- 每次都是全量遍历 fiber tree 收集副作用函数
-
fiber 树实际上是一个单向链表
-
因此 react diff 是单向两次遍历
-
强依赖 Key
-
利用 fiber + messageChannel 实现可中断的渲染
-
一直存在两棵树
-
优化依赖手动配置和时间切片
vue3
-
对于vnode树,生成和遍历是全量的(BFS)。但是对于有对于依赖追踪的局部更新
- 通过 Proxy 收集依赖,当数据变化的时候仅仅更新相关的组件 vnode 渲染和 dom 更新。
- 虽然每次都是全量生成vnode,但是vue通过静态标记和动态分区,静态节点提升等方式限制了 diff 的范围
-
vnode 树实际上是一个双向链表,借鉴 snabbdom
-
vue3 diff 是前后双指针+ LIS 完成的,效率更高
-
没有 key 也可以 diff
-
即使渲染可以批处理,但是每一次渲染都是同步的
-
只有在diff的时候才会存在两棵树
-
编译时动态标记静态和动态分区优化
React 和 Vue 在 diff 上的不同,其实是他们的设计哲学的延伸。
对于React,注重灵活性和控制力。强调显示的控制和可预测性。【开箱即用的高效】
对于Vue,注重渐进式和易用性。最小化开发者的心智负担。【显示控制的可预测性】
vue & snabbdom 对比
vue3 的 diff 算法其实就是 snabbdom 的优化
- 使用编译时标记(静态提升/动态标记),对 diff 范围进行剪枝 ( 这是属性纬度的 )
- 使用区块树进行分区(动态分区/静态分区) ,缩减 diff 范围 ( 这是 vnode 纬度的 )
- 利用 proxy 追踪响应式数据, 进一步缩减 diff 范围
笔者才疏学浅,各位读者多多担待,不吝赐教。