飞道的博客

面试官:了解过vue中的diff算法吗?说说看

574人阅读  评论(0)

一、是什么

diff 算法是一种通过同层的树节点进行比较的高效算法

其有两个特点:

  • 比较只会在同层级进行, 不会跨层级比较

  • 在diff比较的过程中,循环从两边向中间比较

diff 算法的在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

二、比较方式

diff整体策略为:深度优先,同层比较

  1. 比较只会在同层级进行, 不会跨层级比较

  1. 比较的过程中,循环从两边向中间收拢

下面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点的 startIndex 移动到了 C

第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 B 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E

第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndexendIndex 都保持不动

第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F

新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdxnewEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面

三、原理分析

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图

源码位置:src/core/vdom/patch.js


   
  1. function patch(oldVnode, vnode, hydrating, removeOnly) {
  2.      if (isUndef(vnode)) {  // 没有新节点,直接执行destory钩子函数
  3.          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
  4.          return
  5.     }
  6.     let isInitialPatch =  false
  7.      const insertedVnodeQueue = []
  8.      if (isUndef(oldVnode)) {
  9.         isInitialPatch =  true
  10.         createElm(vnode, insertedVnodeQueue)  // 没有旧节点,直接用新节点生成dom元素
  11.     }  else {
  12.          const isRealElement = isDef(oldVnode.nodeType)
  13.          if (!isRealElement && sameVnode(oldVnode, vnode)) {
  14.              // 判断旧节点和新节点自身一样,一致执行patchVnode
  15.             patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  16.         }  else {
  17.              // 否则直接销毁及旧节点,根据新节点生成dom元素
  18.              if (isRealElement) {
  19.                  if (oldVnode.nodeType ===  1 && oldVnode.hasAttribute(SSR_ATTR)) {
  20.                     oldVnode.removeAttribute(SSR_ATTR)
  21.                     hydrating =  true
  22.                 }
  23.                  if (isTrue(hydrating)) {
  24.                      if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
  25.                         invokeInsertHook(vnode, insertedVnodeQueue,  true)
  26.                          return oldVnode
  27.                     }
  28.                 }
  29.                 oldVnode = emptyNodeAt(oldVnode)
  30.             }
  31.              return vnode.elm
  32.         }
  33.     }
  34. }

patch函数前两个参数位为oldVnodeVnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:

  • 没有新节点,直接触发旧节点的destory钩子

  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm

  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点

  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点

下面主要讲的是patchVnode部分


   
  1. function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  2.      // 如果新旧节点一致,什么都不做
  3.      if (oldVnode === vnode) {
  4.        return
  5.     }
  6.      // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
  7.      const elm = vnode.elm = oldVnode.elm
  8.      // 异步占位符
  9.      if (isTrue(oldVnode.isAsyncPlaceholder)) {
  10.        if (isDef(vnode.asyncFactory.resolved)) {
  11.         hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
  12.       }  else {
  13.         vnode.isAsyncPlaceholder =  true
  14.       }
  15.        return
  16.     }
  17.      // 如果新旧都是静态节点,并且具有相同的key
  18.      // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
  19.      // 也不用再有其他操作
  20.      if (isTrue(vnode.isStatic) &&
  21.       isTrue(oldVnode.isStatic) &&
  22.       vnode.key === oldVnode.key &&
  23.       (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  24.     ) {
  25.       vnode.componentInstance = oldVnode.componentInstance
  26.        return
  27.     }
  28.     let i
  29.      const data = vnode.data
  30.      if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  31.       i(oldVnode, vnode)
  32.     }
  33.      const oldCh = oldVnode.children
  34.      const ch = vnode.children
  35.      if (isDef(data) && isPatchable(vnode)) {
  36.        for (i =  0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  37.        if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  38.     }
  39.      // 如果vnode不是文本节点或者注释节点
  40.      if (isUndef(vnode.text)) {
  41.        // 并且都有子节点
  42.        if (isDef(oldCh) && isDef(ch)) {
  43.          // 并且子节点不完全一致,则调用updateChildren
  44.          if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  45.          // 如果只有新的vnode有子节点
  46.       }  else  if (isDef(ch)) {
  47.          if (isDef(oldVnode.text)) nodeOps.setTextContent(elm,  '')
  48.          // elm已经引用了老的dom节点,在老的dom节点上添加子节点
  49.         addVnodes(elm, null, ch,  0, ch.length -  1, insertedVnodeQueue)
  50.          // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
  51.       }  else  if (isDef(oldCh)) {
  52.         removeVnodes(elm, oldCh,  0, oldCh.length -  1)
  53.          // 如果老节点是文本节点
  54.       }  else  if (isDef(oldVnode.text)) {
  55.         nodeOps.setTextContent(elm,  '')
  56.       }
  57.        // 如果新vnode和老vnode是文本节点或注释节点
  58.        // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
  59.     }  else  if (oldVnode.text !== vnode.text) {
  60.       nodeOps.setTextContent(elm, vnode.text)
  61.     }
  62.      if (isDef(data)) {
  63.        if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  64.     }
  65.   }

patchVnode主要做了几个判断:

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容

  • 新节点和旧节点如果都有子节点,则处理比较更新子节点

  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点

  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

子节点不完全一致,则调用updateChildren


   
  1. function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  2.     let oldStartIdx =  0  // 旧头索引
  3.     let newStartIdx =  0  // 新头索引
  4.     let oldEndIdx = oldCh.length -  1  // 旧尾索引
  5.     let newEndIdx = newCh.length -  1  // 新尾索引
  6.     let oldStartVnode = oldCh[ 0// oldVnode的第一个child
  7.     let oldEndVnode = oldCh[oldEndIdx]  // oldVnode的最后一个child
  8.     let newStartVnode = newCh[ 0// newVnode的第一个child
  9.     let newEndVnode = newCh[newEndIdx]  // newVnode的最后一个child
  10.     let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  11.      // removeOnly is a special flag used only by <transition-group>
  12.      // to ensure removed elements stay in correct relative positions
  13.      // during leaving transitions
  14.      const canMove = !removeOnly
  15.      // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
  16.     while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  17.        // 如果oldVnode的第一个child不存在
  18.        if (isUndef(oldStartVnode)) {
  19.          // oldStart索引右移
  20.         oldStartVnode = oldCh[++oldStartIdx]  // Vnode has been moved left
  21.        // 如果oldVnode的最后一个child不存在
  22.       }  else  if (isUndef(oldEndVnode)) {
  23.          // oldEnd索引左移
  24.         oldEndVnode = oldCh[--oldEndIdx]
  25.        // oldStartVnode和newStartVnode是同一个节点
  26.       }  else  if (sameVnode(oldStartVnode, newStartVnode)) {
  27.          // patch oldStartVnode和newStartVnode, 索引左移,继续循环
  28.         patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  29.         oldStartVnode = oldCh[++oldStartIdx]
  30.         newStartVnode = newCh[++newStartIdx]
  31.        // oldEndVnode和newEndVnode是同一个节点
  32.       }  else  if (sameVnode(oldEndVnode, newEndVnode)) {
  33.          // patch oldEndVnode和newEndVnode,索引右移,继续循环
  34.         patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  35.         oldEndVnode = oldCh[--oldEndIdx]
  36.         newEndVnode = newCh[--newEndIdx]
  37.        // oldStartVnode和newEndVnode是同一个节点
  38.       }  else  if (sameVnode(oldStartVnode, newEndVnode)) {  // Vnode moved right
  39.          // patch oldStartVnode和newEndVnode
  40.         patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  41.          // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
  42.         canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  43.          // oldStart索引右移,newEnd索引左移
  44.         oldStartVnode = oldCh[++oldStartIdx]
  45.         newEndVnode = newCh[--newEndIdx]
  46.        // 如果oldEndVnode和newStartVnode是同一个节点
  47.       }  else  if (sameVnode(oldEndVnode, newStartVnode)) {  // Vnode moved left
  48.          // patch oldEndVnode和newStartVnode
  49.         patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  50.          // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
  51.         canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  52.          // oldEnd索引左移,newStart索引右移
  53.         oldEndVnode = oldCh[--oldEndIdx]
  54.         newStartVnode = newCh[++newStartIdx]
  55.        // 如果都不匹配
  56.       }  else {
  57.          if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
  58.          // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
  59.         idxInOld = isDef(newStartVnode.key)
  60.           ? oldKeyToIdx[newStartVnode.key]
  61.           : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
  62.          // 如果未找到,说明newStartVnode是一个新的节点
  63.          if (isUndef(idxInOld)) {  // New element
  64.            // 创建一个新Vnode
  65.           createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  66.          // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
  67.         }  else {
  68.           vnodeToMove = oldCh[idxInOld]
  69.            /* istanbul ignore if */
  70.            if (process.env.NODE_ENV !==  'production' && !vnodeToMove) {
  71.             warn(
  72.                'It seems there are duplicate keys that is causing an update error. ' +
  73.                'Make sure each v-for item has a unique key.'
  74.             )
  75.           }
  76.            // 比较两个具有相同的key的新节点是否是同一个节点
  77.            //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
  78.            if (sameVnode(vnodeToMove, newStartVnode)) {
  79.              // patch vnodeToMove和newStartVnode
  80.             patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
  81.              // 清除
  82.             oldCh[idxInOld] = undefined
  83.              // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
  84.              // 移动到oldStartVnode.elm之前
  85.             canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  86.            // 如果key相同,但是节点不相同,则创建一个新的节点
  87.           }  else {
  88.              // same key but different element. treat as new element
  89.             createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
  90.           }
  91.         }
  92.          // 右移
  93.         newStartVnode = newCh[++newStartIdx]
  94.       }
  95.     }

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

  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:

    • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面

    • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

小结

  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁

  • 通过isSameVnode进行判断,相同则调用patchVnode方法

  • patchVnode做了以下操作:

    • 找到对应的真实dom,称为el

    • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点

    • 如果oldVnode有子节点而VNode没有,则删除el子节点

    • 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el

    • 如果两者都有子节点,则执行updateChildren函数比较子节点

  • updateChildren主要做了以下操作:

    • 设置新旧VNode的头尾指针

    • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作

参考文献

  • https://juejin.cn/post/6881907432541552648#heading-1

  • https://www.infoq.cn/article/udlcpkh4iqb0cr5wgy7f


面试官VUE系列总进度:23/33

面试官:说说对observable的理解

面试官:说说为什么要在列表组件中写 key,其作用是什么?

面试官:说说你对keep-alive的理解是什么?怎么缓存当前的组件?缓存后怎么更新?

面试官:Vue常用的修饰符有哪些?有什么应用场景?

面试官:你有写过自定义指令吗?自定义指令的应用场景有哪些?

面试官:什么是虚拟DOM?如何实现一个虚拟DOM?

篇副有限,扫下方二维码查看往期


转载:https://blog.csdn.net/weixin_44475093/article/details/111714220
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场