关于vue虚拟dom+diff算法

233 阅读4分钟

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

什么是虚拟DOM?

对真实DOM的抽象,以js对象作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上。

在js对象中,虚拟DOM表现为一个object对象,包含标签名tag,属性attrs和子元素对象children。

为什么要用虚拟DOM?

1,操作真实DOM非常慢,代价很高,频繁操作还会出现页面卡顿,影响用户体验。

当用原生api去操作dom的时候,浏览器会从构建DOM树开始从头到尾执行一遍流程。这种情况下如果需要更新10次dom节点,浏览器就要执行10次流程。而通过VNODE,同样更新10个DOM节点,虚拟dom不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到dom树,避免大量的无畏计算。

2,抽象了原本的渲染过程,实现了跨平台的能力,不仅仅局限于浏览器的DOM,可以是安卓和IOS的原生组件,小程序等。

比较两棵虚拟 DOM 树的差异 — diff 算法

(1)深度优先遍历,记录差异

diff 算法用来比较两棵 Virtual DOM 树的差异,如果需要两棵树的完全比较,那么 diff 算法的时间复杂度为O(n^3)。但是在前端当中,你很少会跨越层级地移动 DOM 元素,所以 Virtual DOM 只会对同一个层级的元素进行对比,如下图所示, div 只会和同一层级的 div 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)。

image.png

(2)diff算法整体流程图

image.png

updateChildren方法

采用首尾指针法。新旧节点构成集合,分别有两个指针在首尾start和end。

while循环主要处理了以下五种情景:

  • 当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1
  • 当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1
  • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1
  • 当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1
  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
    • 从旧的 VNode 为 key 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面
    • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

v-for不建议用index作为key值?

场景:当我们在一个ul-li *3列表的基础上进行在列表头部新增 一个li,并且设置key=“index”。会发现新增操作时所有的li都会更新,而不是我们预期的只是新增一个节点。

原因:如果把index作为key,在diff算法时候,会进行olds和news的samenode对比,这一步命中了逻辑,因为现在的新旧两次的首节点key都为0,同样key为1和2的时候页命中了逻辑,导致相同key节点会进行patchvnode更新文本,而原本就有的最后一个节点,却因为老节点中没有4,而被当做新节点新增。所以说前三个都进行了patchvnode更新文本,最后一个节点又重新新增,所有节点都更新了

解决方案:使用一个独一无二的值当做key