为什么要使用 diff 算法

减少 dom 的更新量,找到最小差异部分的 dom ,也就是尽可能的复用旧节点,最后只更新新的部分即可,节省 dom 的新增和删除等操作。

key 主要用在 Vue 的虚拟 DOM 算法中,再新旧节点对比时辨识 VNodes 。如果不使用 key ,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改、复用相同类型元素的算法,而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。此外有相同父元素的子元素必须有独特的 key ,重复的 key 会造成渲染错误。

key 的主要作用是为了高效的更新虚拟 dom ,其原理是 vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个 patch 过程更加高效,减少 DOM 操作量,提高性能。

diff 比较流程

sameVnode

在 diff 的过程中,首先需要判断两个节点是否是相同类型的节点,用到的方法如下:

  1. function sameVnode(a, b) {
  2. return (
  3. a.key === b.key && // key 值
  4. ((a.tag === b.tag && // 标签名
  5. a.isComment === b.isComment && // 是否为注释节点
  6. isDef(a.data) === isDef(b.data) && // 是否都定义了 data
  7. sameInputYpe(a, b)) || // 当标签是<input>的时候,type必须相同
  8. (isTrue(a.isAsyncPlaceholder) &&
  9. a.asyncFactory === b.asyncFactory &&
  10. isUndef(b.asyncFactory.error)))
  11. );
  12. }

如果两个节点都是一样的,那么就深入检查它们的子节点。如果两个节点不一样那就说明 VNode 完全被改变了,可以直接替换 oldVnode 。

patchVnode

当我们确定两个节点值得比较之后我们会对两个节点指定 patchVnode 方法

  1. patchVnode (oldVnode, vnode) {
  2. const el = vnode.el = oldVnode.el
  3. let i, oldCh = oldVnode.children, ch = vnode.children
  4. if (oldVnode === vnode) return
  5. if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
  6. api.setTextContent(el, vnode.text)
  7. }else {
  8. updateEle(el, vnode, oldVnode)
  9. if (oldCh && ch && oldCh !== ch) {
  10. updateChildren(el, oldCh, ch)
  11. }else if (ch){
  12. createEle(vnode) //create el's children dom
  13. }else if (oldCh){
  14. api.removeChildren(el)
  15. }
  16. }
  17. }

这个函数做了以下事情:

  • 找到对应的真实 dom ,称为 el
  • 判断 Vnode 和 oldVnode 是否指向同一个对象,如果是,那么直接 return
  • 如果它们都有文本节点并且不相等,那么将 el 的文本节点设置为 Vnode 的文本节点
  • 如果 oldVnode 有子节点而 Vnode 没有,则删除 el 的子节点
  • 如果 oldVnode 没有子节点而 Vnode 有,则将 Vnode 的子节点真实化之后添加到 el
  • 如果两者都有子节点,则执行 updateChildren 函数比较子节点

    updateChildren

    这个函数做了:

  • 将 Vnode 的子节点 Vch 和 oldVnode 的子节点 oldCh 提取出来

  • oldCh 和 vCh 各有两个头尾的变量 StartIdx 和 EndIdx ,它们的 2 个变量相互比较,一共有 4 种方式。如果 4 种比较都没匹配,如果设置了 key ,就会用 key 进行比较,在比较的过程中,变量会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 vCh 至少有一个已经遍历完了,就会结束比较。

循环中的具体逻辑:

  1. 当新老 Vnode 节点的 start 满足 sameVnode 时,直接 patchVnode 即可,同事新老 Vnode 节点的开始索引都加 1
  2. 当新老 Vnode 节点的 end 满足 sameVnode 时,同样直接 patchVnode 即可,同时新老 Vnode 节点的结束索引都减 1
  3. 当老 Vnode 节点的 start 和新 Vnode 节点的 end 满足 sameVnode 时,这说明这次数据更新后 oldStartVnode 已经跑到了 oldEndVnode 后面去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 Vnode 节点开始索引加 1 ,新 Vnode 节点的结束索引减 1
  4. 当老 Vnode 节点的 end 和新 Vnode 节点的 start 满足 sameVnode 时,这说明这次数据更新后 oldEndVnode 跑到了 oldStartVnode 的前面去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 Vnode 节点的结束索引减 1 ,新 Vnode 节点的开始索引加 1

如果都不满足以上四种情形,那说明没有相同的节点可以复用,于是则通过查找事先建立好的以旧的 Vnode 为 key 值,对应 index 序列为 value 值的哈希表。从这个哈希表中找到与 newStartVnode 一致 key 的旧的 Vnode 节点,如果两者满足 sameVnode 的条件,在进行 patchVnode 的同时会将这个真实 dom 移动到 oldStartVnode 对应的真实 dom 的前面;如果没有找到,则说明当前索引下的新的 Vnode 节点在旧的 Vnode 队列中不存在,无法进行节点的复用,那么就只能 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置。

最后遍历结束后,根据新老节点的数目不同,做相应的节点添加或者删除。若新节点数目大于老节点则需要把多出来的节点创建出来加入到真实 dom 中,反之若老节点数目大于新节点则需要把多出来的老节点从真实 dom 中删除。至此整个 diff 过程就已经全部完成了。

参考链接:

key 的使用场景

  • v-for(加速虚拟 DOM 渲染)
  • 强制替换 element 或 component(响应式系统没有监听到的数据,用 +new Date() 生成时间戳作为 key ,手动强制触发重新渲染