vue diff アルゴリズムを理解していますか?原理分析

青灯夜游
リリース: 2022-08-08 19:21:01
転載
3114 人が閲覧しました

diff アルゴリズムは、ツリー ノードを同じレベルで比較する効率的なアルゴリズムであり、ツリーをレイヤーごとに検索して横断する必要がなくなります。では、diff アルゴリズムについてどれくらい知っていますか?次の記事では、vue の差分アルゴリズムについて詳しく説明します。お役に立てば幸いです。

vue diff アルゴリズムを理解していますか?原理分析

1.

diff とは何ですか? このアルゴリズムは、同じレベルのツリー ノードの比較です。効率的なアルゴリズム。 (学習ビデオ共有: vue ビデオ チュートリアル)

これには 2 つの特徴があります:

  • 比較は同じレベルでのみ実行され、比較されません。レベル間
  • 差分比較プロセス中に、ループは両側から中央まで比較します。

diff このアルゴリズムは、## の多くのシナリオに適用されます。 #vue、仮想 dom に作用し、新旧の VNode ノードを実際の dom ノード比較 ## にレンダリングします。 ## 2. 比較方法

diff

全体的な戦略は次のとおりです。深さを優先し、同じレベルで比較します。

比較は次のとおりです。同じレベルで実行され、レベル間では比較されません

vue diff アルゴリズムを理解していますか?原理分析## 比較プロセス中に、両方のレベルからループが縮小します。

次は、

diffvue diff アルゴリズムを理解していますか?原理分析 アルゴリズムを通じて更新された

vue

の例です。 新旧 VNode ノードは以下のとおりです。

最初のサイクルの後、古いノード D がは新しいノード D と同じであり、古いノード D は

diffvue diff アルゴリズムを理解していますか?原理分析 実ノードの後の最初のノードとして直接再利用され、古いノード

endIndex

は C に移動されます。新しいノードの startIndex は、2 番目のサイクルの後、C

に移動されます。古いノードの終わりと新しいノードの始まり (両方とも C)同様に、

diffvue diff アルゴリズムを理解していますか?原理分析 の後に作成した実ノード C を、最初に作成した D ノードの後ろに挿入します。同時に、古いノードの

endIndex

は B に移動し、新しいノードの startIndex は E

に移動しました。 # 3 番目のサイクルで、E が見つかりました。見つからない場合は、新しい実ノード E を直接作成し、2 番目に作成した C ノードの後に​​挿入することしかできません。同時に、新しいノードの

startIndexvue diff アルゴリズムを理解していますか?原理分析 が A に移動されます。古いノードの

startIndex

endIndex は変更されません。

4 番目のサイクルでは、新しいノードと古いノードの始まりです。が見つかったので (すべて A と同じです)、

diffvue diff アルゴリズムを理解していますか?原理分析 は A の実ノードを作成し、以前に作成した E ノードの後ろに挿入します。同時に、古いノードの

startIndex

が B に移動され、新しいノードの startIndex が B に移動されます。

5 番目のサイクルでも状況は同じです。4 番目のループも同じです。したがって、

diffvue diff アルゴリズムを理解していますか?原理分析 は B 実ノードを作成し、前に作成した A ノードの後ろに挿入します。同時に、古いノードの

startIndex

は C に移動され、新しいノードの startIndex は F

に移動されます。新しいノードの ##startIndexvue diff アルゴリズムを理解していますか?原理分析 はすでに

endIndex

より大きいため、newStartIdxnewEndIdx の間のすべてのノード、つまりノードを作成する必要があります。 F、F ノードに対応する実際のノードを直接作成し、それを B ノードの後ろに置きます

3. 原理分析vue diff アルゴリズムを理解していますか?原理分析

データが変更されると、set メソッドは

Dep.notify

すべてのサブスクライバ Watcher を呼び出し、サブスクライバはパッチを適用するために patch を呼び出します。実際の DOM を更新し、対応するビューを更新しますソース コードの場所: src/core/vdom/patch.js<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false">function patch(oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数 if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素 } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement &amp;&amp; sameVnode(oldVnode, vnode)) { // 判断旧节点和新节点自身一样,一致执行patchVnode patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 否则直接销毁及旧节点,根据新节点生成dom元素 if (isRealElement) { if (oldVnode.nodeType === 1 &amp;&amp; oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } oldVnode = emptyNodeAt(oldVnode) } return vnode.elm } } }</pre><div class="contentsignin">ログイン後にコピー</div></div>

patch

最初の 2 つ関数のパラメータは

oldVnode

Vnode で、それぞれ新しいノードと以前の古いノードを表し、主に 4 つの判断を行います。

  • 没有新节点,直接触发旧节点的destory钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点

下面主要讲的是patchVnode部分

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果新旧节点一致,什么都不做
    if (oldVnode === vnode) {
      return
    }
    // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
    const elm = vnode.elm = oldVnode.elm
    // 异步占位符
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }
    // 如果新旧都是静态节点,并且具有相同的key
    // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
    // 也不用再有其他操作
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 如果vnode不是文本节点或者注释节点
    if (isUndef(vnode.text)) {
      // 并且都有子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 并且子节点不完全一致,则调用updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        // 如果只有新的vnode有子节点
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
        // elm已经引用了老的dom节点,在老的dom节点上添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
        // 如果老节点是文本节点
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, &#39;&#39;)
      }
      // 如果新vnode和老vnode是文本节点或注释节点
      // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }
ログイン後にコピー

patchVnode主要做了几个判断:

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则处理比较更新子节点
  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

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

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 旧头索引
    let newStartIdx = 0 // 新头索引
    let oldEndIdx = oldCh.length - 1 // 旧尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一个child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
    let newStartVnode = newCh[0] // newVnode的第一个child
    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly
    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果oldVnode的第一个child不存在
      if (isUndef(oldStartVnode)) {
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      // 如果oldVnode的最后一个child不存在
      } else if (isUndef(oldEndVnode)) {
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]
      // oldStartVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // patch oldStartVnode和newStartVnode, 索引左移,继续循环
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      // oldEndVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // patch oldEndVnode和newEndVnode,索引右移,继续循环
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      // oldStartVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      // 如果oldEndVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      // 如果都不匹配
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 如果未找到,说明newStartVnode是一个新的节点
        if (isUndef(idxInOld)) { // New element
          // 创建一个新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== &#39;production&#39; && !vnodeToMove) {
            warn(
              &#39;It seems there are duplicate keys that is causing an update error. &#39; +
              &#39;Make sure each v-for item has a unique key.&#39;
            )
          }
          // 比较两个具有相同的key的新节点是否是同一个节点
          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          // 如果key相同,但是节点不相同,则创建一个新的节点
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }
        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }
ログイン後にコピー

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 节点再分情况操作

(学习视频分享:web前端开发编程基础视频

以上がvue diff アルゴリズムを理解していますか?原理分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:vue3js
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート