是什么?

Virtual DOM 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,省略手动 DOM 操作可以大大提高开发效率

diff 算法

生成补丁、更新差异的过程统称为 diff 算法
image.png

  • 真实的 DOM 首先会映射为虚拟 DOM;
  • 当虚拟 DOM 发生变化后,就会根据差异计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
  • 根据 patch 去更新真实的 DOM,反馈到用户的界面上。

React 对 DOM diff 做了三个预设来降低时间复杂度

  1. 只对同级元素进行 Diff,跨层级的 DOM 节点 React 不会复用
  2. 两个不同类型的元素会创建出不同的树。如果元素类型改变,那么 React 会销毁旧元素及其子孙节点并且重新构造新元素及其子孙节点
  3. 通过设置 key 可以让某些子元素在不同渲染下保持稳定

React DOM diff 分为多节点 diff单节点 diff

  • newChild 类型为 object,number,string——会进行单节点 diff =》reconcileSingleElement
  • newChild 类型为 Array =》同级有多个节点 —— 会进行多节点 diff =》reconcileChildrenArray

    单节点 diff

    image.png

判断是否可以复用:首先判断 key 是否相同,如果相同,再判断 type 是否相同,只有都相同的 DOM 节才能复用
key 相同,type 不同,会把 child 及其兄弟节点全部标记为删除;key 不同,仅把 child 标记为删除。(child 是老 fiber 节点)

多节点 diff

多节点 diff 分为三种情况

  1. 节点更新:节点属性改变/节点类型改变
  2. 节点新增或减少:新增/删除
  3. 节点位置的变化

这里 React 会优先判断当前节点是否属于更新,因为更新的频率更高。
所以分为了两轮遍历

  1. 第一轮遍历:处理更新的节点
  2. 第二轮遍历:处理剩下的不属于更新的节点

第一轮遍历

  1. 遍历 newChildrenoldFiber进行比较,判断是否可以复用
  2. 如果可以复用,用下一个和oldFiber.sibling 进行比较,如果可以复用继续遍历
  3. 如果出现不可复用,分为两种情况
    1. key 不同导致的,立即跳出整个遍历,第一轮遍历就结束了
    2. key 相同 type 不同导致的,将 oldFiber标记为 DELETION,并继续遍历
  4. 如果newChildren遍历完了,或者oldFiber遍历完了,跳出遍历,第一轮遍历结束

第二轮遍历
如果 newChildren 和 oldFiber 都遍历完了
只需要在第一轮遍历进行更新,Diff 结束。

如果 newChildren 遍历完了
说明本次更新相比之前节点数减少了,有节点被删除了。
遍历剩余的 oldFiber,依次标记为 DELETION

如果 oldFiber 遍历完了
说明还有新增节点。有插入。
遍历剩下的 newChildren 为生成的 workInProgress fiber 标记为 Placement

**newChildren****oldFiber**都没遍历完
说明有节点在这次更新中更改了位置
精髓——mapRemainingChildren。Vue3 dom diff 抄的地方
为了快速的找到 key 对应的 oldFiber,我们将所有还未处理的 oldFiber 存入以 key 为 key,oldFiber 为 value 的 Map 中
我们要寻找移动的节点,而节点的移动是以lastPlacedIndex为参照物
lastPlacedIndex:最后一个可复用的节点在 oldFiber 中位置的索引,初始为 0。
我们用 oldIndex 表示 遍历到的可复用节点在 oldFiber 中的位置索引。如果 oldIndex < lastPlacedIndex,表示本次更新该节点需要向右移动。如果 oldIndex >= lastPlacedIndex,则 lastPlacedIndex = oldIndex。

Vue3

  1. 从前往后比较,不同的停止比较
  2. 从后往前比较,不同的停止比较
  3. 如果 c1(old) 遍历结束了,说明需要插入,执行 mount
  4. 如果 c2(new) 遍历完了,说明需要删除,执行 unmount
  5. 如果都有剩余,
    1. 找出需要删除的节点,执行 unmount
    2. 找出新旧节点的对应关系,利用 ”最长递增子序列“优化节点的移动、新增——也就是从没比较完的两段中再找出不需要比较的部分。