[Vue2.0 ソースコード学習] 仮想 DOM の記事 - Vue の DOM - 子ノードの最適化と更新

1 はじめに

前回の記事では、新しいノードVNodeと古いノードの両方がoldVNode要素ノードであり、両方に子ノードが含まれる場合、Vueその子ノードは

最初に外側のループnewChildren配列、次に内側のループ配列。外側の配列の子ノードがループされるoldChildrenたびに、内側の配列に移動してそれと同じ子ノードがあるかどうかを確認し、最後に別の操作を実行します。さまざまな状況に応じて。newChildrenoldChildren

前回の記事の最後で、この方法で問題は解決できるものの、まだ最適化できる箇所があることも述べました。たとえば、子ノードの数が多い場合、ループ アルゴリズムの時間計算量が非常に大きくなり、パフォーマンスの向上にはつながりません。もちろん私もこれに気づいて最適化しましたので、この記事ではVue子ノードの更新を最適化するVue方法を学びましょう。

2. 最適化戦略の概要

次のように、新しいnewChildren配列と古いoldChildren配列があるとします。

newChildren = ['新子节点1','新子节点2','新子节点3','新子节点4']
oldChildren = ['旧子节点1','旧子节点2','旧子节点3','旧子节点4']

最適化の前に解決策に従う場合、次の操作は次のようになります。まずnewChildren配列をループし、最初の新しい子ノード 1 を取得し、次に最初の新しい子ノード 1 を使用してoldChildren配列内の古い子ノードをたどります。運が少しよければ、oldChildren配列内の最初の古い子ノード 1 が最初の新しい子ノード 1 と偶然同じであれば、全員が満足し、さらにループダウンせずに直接処理できます。次に、運が少し悪い場合は、oldChildrenループ内の 4 番目の古い子ノード 4 が最初の新しい子ノード 1 と同じになるまで、この時点でさらに 4 回ループします。もう少し極端な状況を想像してみてもよいでしょう。newChildren配列もoldChildren配列内の最初の 3 つのノードも変更されていないが、4 番目のノードだけが変更されている場合は、16 回ループし、16 番目でのみ判明します。以下の図に示すように、新しいノード 4 は古いノード 4 と同じであり、更新する必要があります。
ここに画像の説明を挿入

上記の例では、子ノードが 4 つしかないため、問題がないように見えますが、子ノードの数が多くなると、アルゴリズムの時間計算量が非常に高くなり、パフォーマンスの向上にはつながりません。 。

では、どうすれば最適化できるのでしょうか? 実際、このように考えることができます。2 つの配列を順番にループする代わりにnewChildrenoldChildren最初に 2 つの配列内の特別な位置にある子ノードを比較できます。次に例を示します。

  • まず、newChildren配列内のすべての未処理の子ノードの最初の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードoldChildrenを比較し、それらが同じであれば、直接ノードの更新操作に入ります。
  • 異なる場合は、newChildren配列内のすべての未処理の子ノードの最後の子ノードと、oldChildren配列内のすべての未処理の子ノードの最後の子ノードを比較し、同じであれば、直接ノードの更新操作に入ります。
  • それらが異なる場合は、newChildren配列内のすべての未処理の子ノードの最後の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードoldChildrenを比較します。それらが同じである場合は、ノードの更新操作に直接進みます。更新後次に、oldChildren配列内のノードをnewChildren配列内のノードと同じ位置に移動します。
  • それらが異なる場合は、配列newChildren内のすべての未処理の子ノードの最初の子ノードoldChildrenと、配列内のすべての未処理の子ノードの最後の子ノードを比較し、それらが同じである場合は、ノードの更新操作を直接入力します。oldChildren配列内のノードをnewChildren配列内のノードと同じ位置に移動します。
  • 最後の 4 つのケースを試した後、それでも異なる場合は、前のループ方法に従ってノードを見つけます。

プロセスを次の図に示します。

ここに画像の説明を挿入

上の画像では、次のように配置します。

  • newChildren配列内のすべての未処理の子ノードの最初の子ノードが次のように呼び出されます。
  • newChildren配列内のすべての未処理の子ノードの最後の子ノードは、 new post; と呼ばれます。
  • oldChildren配列内のすべての未処理の子ノードの最初の子ノードは、次のように呼ばれます。
  • oldChildren配列内のすべての未処理の子ノードの最後の子ノードは、old post と呼ばれます。

さて、上記の概念を理解したので、それがどのように実装されるかを見てみましょう。

3. 新戦線対旧戦線

newChildren配列内のすべての未処理の子ノードの最初oldChildrenの子ノードと、配列内のすべての未処理の子ノードの最初の子ノードを比較します。それらが同じであれば、それは素晴らしいことです。前の記事で説明した更新ノードに直接移動し新旧ノードの位置が同じ場合はノードの移動操作は必要ありませんが、異なっていても問題ありませんので、次の 3 つのケースを試してください。ここに画像の説明を挿入

4. 新しい投稿と古い投稿

newChildren配列内のすべての未処理の子ノードの最後のoldChildren子ノードと、配列内のすべての未処理の子ノードの最後の子ノードを比較し、同じであれば、ノードを更新する操作を直接入力します。新しいノードと古いノードは同じであるためone ノードの位置も同じなので、ノードを移動する必要はありません。異なる場合は、後で続けて試してください。ここに画像の説明を挿入

5. 新しい後と古い前線

newChildren配列内のすべての未処理の子ノードの最後の子ノードと、配列内のすべての未処理の子ノードの最初の子ノードを比較しoldChildren、それらが同じである場合は、ノードを更新する操作を直接入力し、配列を更新しますoldChildren。ノードをnewChildren配列内のノードと同じ位置に移動します。
ここに画像の説明を挿入

このとき、ノードを移動する操作が表示されますが、ノードの移動で最も重要なのは、移動する場所を見つけることです。更新ノードはVNode新しいノードに基づいて行われ、次に古いノードを操作して古いノードを新しいノードと同じにoldVNodeするoldVNodeVNode必要があることを繰り返し強調してきました現在の状況は、newChildren配列内の最後の子ノードがoldChildren配列内の最初の子ノードと同じである場合、oldChildren図に示すように、最初の子ノードを配列内の最後の子ノードの位置に移動する必要があります。下:

ここに画像の説明を挿入

この図から、配列内の未処理のノードをすべて移動した後で、oldChildren配列内の最初の子ノードを移動する必要があることがわかります

比較後、2 つのノードがまだ同じノードではないことが判明した場合は、最後のケースの試行を続けます。

6. 新しい前と古い後

newChildren配列内のすべての未処理の子ノードの最初の子ノードと、oldChildren配列内のすべての未処理の子ノードの最後の子ノードを比較します。それらが同じである場合は、ノードの更新操作に直接進み、次のノードを更新します配列oldChildrenノードをnewChildren配列内のノードと同じ位置に移動します。

ここに画像の説明を挿入

同様に、この場合のノードの位置移動のロジックは、「新しい後と古い前」のロジックに似ています。つまり、newChildren配列内の最初の子ノードが配列oldChildren内の最後の子ノードと同じである場合、次のようにする必要があります。oldChildren以下の図に示すように、最後の子ノードが最初の子ノードの位置に移動します。

ここに画像の説明を挿入

この図から、配列内のすべての未処理のノードの前に、oldChildren配列内の最後の子ノードを移動する必要があることがわかります

OK、上記はサブノードの比較と更新の最適化戦略の 4 つのケースです。上記の 4 つのケースを 1 つずつ試しても同じノードが見つからない場合は、前のループ方法を検索します。

7. ソースコードに戻ります

アイデアを分析してロジックを明確にした後、ソース コードに戻って、ソース コードに実装されているロジックが分析したものと同じかどうかを確認してみましょう。ソースコードは次のとおりです。

// 循环更新子节点
  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    
    
    let oldStartIdx = 0               // oldChildren开始索引
    let oldEndIdx = oldCh.length - 1   // oldChildren结束索引
    let oldStartVnode = oldCh[0]        // oldChildren中所有未处理节点中的第一个
    let oldEndVnode = oldCh[oldEndIdx]   // oldChildren中所有未处理节点中的最后一个

    let newStartIdx = 0               // newChildren开始索引
    let newEndIdx = newCh.length - 1   // newChildren结束索引
    let newStartVnode = newCh[0]        // newChildren中所有未处理节点中的第一个
    let newEndVnode = newCh[newEndIdx]  // newChildren中所有未处理节点中的最后一个

    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

    if (process.env.NODE_ENV !== 'production') {
    
    
      checkDuplicateKeys(newCh)
    }

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    
      if (isUndef(oldStartVnode)) {
    
    
        oldStartVnode = oldCh[++oldStartIdx] // 如果oldStartVnode不存在,则直接跳过,比对下一个
      } else if (isUndef(oldEndVnode)) {
    
    
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
    
    
        // 如果新前与旧前节点相同,就把两个节点进行patch更新
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
    
    
        // 如果新后与旧后节点相同,就把两个节点进行patch更新
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
    
     // Vnode moved right
        // 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
    
     // Vnode moved left
        // 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
    
    
        // 如果不属于以上四种情况,就进行常规的循环比对patch
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 如果在oldChildren里找不到当前循环的newChildren里的子节点
        if (isUndef(idxInOld)) {
    
     // New element
          // 新增节点并插入到合适位置
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
    
    
          // 如果在oldChildren里找到了当前循环的newChildren里的子节点
          vnodeToMove = oldCh[idxInOld]
          // 如果两个节点相同
          if (sameVnode(vnodeToMove, newStartVnode)) {
    
    
            // 调用patchVnode更新节点
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            // canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
    
    
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
    
    
      /**
       * 如果oldChildren比newChildren先循环完毕,
       * 那么newChildren里面剩余的节点都是需要新增的节点,
       * 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
       */
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
    
    
      /**
       * 如果newChildren比oldChildren先循环完毕,
       * 那么oldChildren里面剩余的节点都是需要删除的节点,
       * 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
       */
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

ソースコードを読む前に、まず次のような概念があります。つまり、先ほど述べた最適化戦略では、ノードを前からまたは後ろから比較し、比較が成功した場合に更新プロセスが実行されるということです。つまり、最初のものを処理することも、最後のものを処理することもできるため、循環するときは、単に前から後ろ、または後ろから前に循環するだけではなく、両側から前に循環することになります。真ん中。

では、両側から中央までループするにはどうすればよいでしょうか? 以下の写真をご覧ください。

ここに画像の説明を挿入

まず、4 つの変数を準備します。

  • newStartIdx:newChildren配列内の開始位置の添字。
  • newEndIdx:newChildren配列内の終了位置の添字。
  • oldStartIdx:oldChildren配列内の開始位置の添字。
  • oldEndIdx:oldChildren配列内の終了位置の添字。

ループすると、ノードを処理するたびに添字が図の矢印の方向に移動し、開始位置で示されるノードの処理が完了すると、1つ後ろに移動し、終了位置で示されるノードが処理されます。位置は処理後に 1 つ前に移動します。最適化戦略は古いノードと新しいノードをペアで更新することなので、1 回の更新で 2 つのノードが移動します。もっと率直に言うと、newStartIdx後方oldStartIdxにのみ移動でき (加算のみ)、newEndIdx前方oldEndIdxにのみ移動できます (減算のみ)。

開始位置が終了位置より大きい場合は、すべてのノードが通過されたことを意味します。

OK、この概念に基づいて、ソース コードを読み始めます。

  1. oldStartVnode存在しない場合は、直接スキップしてoldStartIdx1 を追加し、次の値を比較します。

    // 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          
          
     if (isUndef(oldStartVnode)) {
          
          
       oldStartVnode = oldCh[++oldStartIdx]
     }
    }
    
  2. 存在しない場合はoldEndVnode直接スキップされ、oldEndIdx前のものと比較して 1 減算されます。

    else if (isUndef(oldEndVnode)) {
          
          
        oldEndVnode = oldCh[--oldEndIdx]
    }
    
  3. 新しいフロント ノードが古いフロント ノードと同じである場合は、2 つのノードを更新しpatch、同時に合計に 1 を加えoldStartIdxnewStartIdx位置を 1 つ戻します。

    else if (sameVnode(oldStartVnode, newStartVnode)) {
          
          
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
    }
    
  4. 新しいノードが古いノードと同じである場合は、2 つのノードを更新しpatch、合計を 1 減らしてoldEndIdxnewEndIdx1 つ前に移動します。

    else if (sameVnode(oldEndVnode, newEndVnode)) {
          
          
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
    }
    
  5. 新しいノードが古いノードと同じである場合は、最初に 2 つのノードを更新しpatch、次に古いノードをoldChilrenすべての未処理ノードの後ろに移動し、最後にoldStartIdx1 を追加し、1 位置戻しnewEndIdx、1 を減算し、1 位置前に移動します。

    else if (sameVnode(oldStartVnode, newEndVnode)) {
          
          
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
    }
    
  6. 新しいフロント ノードが古いバック ノードと同じである場合は、最初に 2 つのノードを更新しpatch、次に古いバック ノードをoldChilrenすべての未処理ノードの前に移動し、最後にnewStartIdx1 を追加し、1 つ前の位置に戻しoldEndIdx、1 を減算して前に進みます。 1つのポジション

    else if (sameVnode(oldEndVnode, newStartVnode)) {
          
           // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
    }
    
  7. 上記4つの状況に当てはまらない場合は、通常のサイクル比較を行ってください。patch

  8. ループ内でoldStartIdxこの値がより大きい場合oldEndIdx、これはサイクルが以前oldChildrenよりも完了していることを意味しnewChildrennewChildrenループ内の残りのノードは追加する必要があるノードであり、[newStartIdx, newEndIdx]間にあるすべてのノードがDOMループに挿入されます。

    if (oldStartIdx > oldEndIdx) {
          
          
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    }
    
  9. サイクル内でnewStartIdxこの値がより大きい場合、サイクルが以前よりも完了したnewEndIdxことを意味しそのサイクル内の残りのノードが削除する必要があるノードとなり、その間にあるすべてのノードが削除されます。newChildrenoldChildrenoldChildren[oldStartIdx, oldEndIdx]

    else if (newStartIdx > newEndIdx) {
          
          
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
    

OK、処理が完了しました。ソースコード内の処理ロジックは、以前に解析したロジックと同じであることがわかります。

8. まとめ

この記事では、Vue中性子ノード更新の最適化戦略を紹介しましたが、Vue二重ループ内の大量のデータと時間の複雑さの増加によって引き起こされるパフォーマンスの問題を回避するために、4 つの特別な位置を比較することを選択したことがわかりました。はい、新しいフロントと古いフロント、新しいリアと古いリア、新しいリアと古いフロント、新しいフロントと古いリアです。それぞれのケースについて、グラフィックとテキストの形式でロジックを分析しました。最後に、ソース コードに戻り、ソース コードを読んで分析が正しいことを確認します。幸いなことに、以前の分析の各ステップのソース コードで対応する実装が見つかりました。これにより、分析が正しいことを確認できます。以上がVue真ん中の処理patch、つまりDOM-Diffアルゴリズムの内容のすべてですが、この部分のソースコードをもう一度読んでいただくと、より明確に理解できると思います。

おすすめ

転載: blog.csdn.net/weixin_46862327/article/details/130899256