Vue diffアルゴリズムの完全な分析

Vue diffアルゴリズムの完全な分析

序文

Vue は仮想 DOM を使用して実際の DOM に対する操作数を減らし、ページ操作の効率を向上させることがわかっています。今日は、ページデータが変更されたときに Vue がどのように DOM を更新するかを見ていきます。 Vue と React は、DOM を更新するときに基本的に同じアルゴリズムを使用します。どちらも snabbdom に基づいています。 ページ上のデータが変更されても、Vue はすぐにはレンダリングしません。代わりに、diff アルゴリズムを使用して、変更する必要がない部分と、変更して更新する必要がある部分を決定します。更新する必要がある DOM 部分のみを更新する必要があります。これにより、不要な DOM 操作が大幅に削減され、パフォーマンスが大幅に向上します。 Vue は、実際の DOM を抽象化した抽象ノード VNode を使用します。特定のプラットフォームに依存しません。ブラウザ プラットフォーム、weex、またはノード プラットフォームでもかまいません。また、このような抽象 DOM ツリーを作成、削除、変更することもでき、フロントエンドとバックエンドの同型性も実現できます。

Vue ビューの更新

Vue 1.x では、各データが Watcher に対応し、Vue 2.x ではコンポーネントが Watcher に対応することがわかっています。このように、データが変更されると、set 関数が Dep の notification 関数をトリガーして Watcher に通知し、vm._update(vm._render(), hydrating) メソッドを実行してビューを更新します。_update メソッドを見てみましょう。

Vue.prototype._update = 関数 (vnode: VNode、hydrating?: ブール値) {
  const vm:コンポーネント = this
  定数 prevEl = vm.$el
  定数 prevVnode = vm._vnode
  定数restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vノード
  // Vue.prototype.__patch__ がエントリポイントに挿入されます
  // 使用されるレンダリング バックエンドに基づきます。
  /*バックエンドレンダリングに基づいて、Vue.prototype.__patch__ がエントリポイントとして使用されます*/
  if (!prevVnode) {
    // 初期レンダリング
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } それ以外 {
    // 更新
    vm.$el = vm.__patch__(前のVnode、vnode)
  }
  アクティブインスタンスを復元する()
  // __vue__ 参照を更新
  /*新しいインスタンスオブジェクトの__vue__を更新します*/
  if (前El) {
    prevEl.__vue__ = null
  }
  (vm.$el)の場合{
    vm.$el.__vue__ = vm
  }
  // 親が HOC の場合は、その $el も更新します
  (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) の場合 {
    vm.$parent.$el = vm.$el
  }
  // 更新されたフックは、子が確実に実行されるようにスケジューラによって呼び出されます。
  // 親の更新フックで更新されました。
}

明らかに、_update メソッドは渡された Vnode を使用して古い Vnode を修正することがわかります。 次に、パッチ関数で何が起こるかを見てみましょう。

パッチ

パッチ関数は、古いノードと新しいノードを比較し、どのノードを変更する必要があるかを判断します。これらのノードのみを変更する必要があるため、DOM をより効率的に更新できます。まずはコードを見てみましょう。

関数 patch (oldVnode, vnode, hydrating, removeOnly) を返す {
  /*vnode が存在しない場合は、ノードを削除するために破壊フックを呼び出します*/
  (vnodeが未定義の場合){
    if (isDef(oldVnode)) は、invokeDestroyHook(oldVnode) を実行します。
    戻る
  }

  isInitialPatch = false とします
  const 挿入VnodeQueue = []

  /*oldVnode が存在しない場合は、新しいノードを直接作成します*/
  if (isUndef(oldVnode)) {
    // 空のマウント(おそらくコンポーネント)、新しいルート要素を作成する
    isInitialPatch = true
    createElm(vnode、挿入されたVnodeQueue)
  } それ以外 {
    /*古い VNode に nodeType があるかどうかをマークします*/
    定数 isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 既存のルートノードにパッチを適用する
      /*同じノードの場合は、既存のノードを直接変更します*/
      patchVnode(古いVnode、vnode、挿入されたVnodeQueue、null、null、removeOnly)
    } それ以外 {
      実要素の場合
        // 実際の要素にマウントする
        // これがサーバー側でレンダリングされたコンテンツであるかどうか、また実行できるかどうかを確認します
        // 水分補給は成功しました。
        oldVnode.nodeType === 1 の場合 && oldVnode.hasAttribute(SSR_ATTR) {
          /*古い VNode がサーバーレンダリング要素である場合、hydrating は true に設定されます*/
          古いVnode.removeAttribute(SSR_ATTR)
          保湿 = 本当
        }
        if (isTrue(水分補給)) {
          /*実際のDomにマージする必要があります*/
          if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
            /*挿入フックを呼び出す*/
            挿入フックを呼び出します(vnode、挿入されたVnodeQueue、true)
            古いVnodeを返す
          } そうでない場合 (process.env.NODE_ENV !== 'production') {
            警告(
              「クライアント側でレンダリングされた仮想DOMツリーが一致しません」+
              'サーバーレンダリングされたコンテンツ。これはおそらく不正な' +
              'HTML マークアップ、たとえばブロックレベル要素を ' + 内にネストする
              '<p>、または欠落している<tbody>。水分補給を中止し、' + を実行します。
              「完全なクライアント側レンダリング。」
            )
          }
        }
        // サーバー側でレンダリングされていないか、ハイドレーションに失敗しました。
        // 空のノードを作成して置き換える
        /*サーバー側レンダリングでない場合、または実際のDOMへのマージに失敗した場合は、空のVNodeノードを作成して置き換えます*/
        古いVnode = 空のノードAt(古いVnode)
      }

      // 既存の要素を置き換える
      /*既存の要素を置き換える*/
      定数 oldElm = oldVnode.elm
      定数 parentElm = nodeOps.parentNode(oldElm)

      // 新しいノードを作成する
      エルムを作成します(
        vノード、
        挿入されたVnodeQueue、
        // 極めて稀なエッジケース: 古い要素が
        // 遷移を終了します。遷移 + を組み合わせた場合にのみ発生します。
        // キープアライブ + HOC。(#4590)
        oldElm._leaveCb ? null : parentElm、
        nodeOps.nextSibling(古いElm)
      )

      // 親プレースホルダーノード要素を再帰的に更新する
      (vnode.parent が定義されていない場合)
        /*コンポーネントのルートノードが置き換えられ、親ノード要素をトラバースして更新されます*/
        祖先 = vnode.parent とする
        const パッチ可能 = isPatchable(vnode)
        (祖先){
          (i = 0 とします; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](祖先)
          }
          祖先.elm = vnode.elm
          if (パッチ可能) {
            /*作成コールバックを呼び出す*/
            (i = 0 とします; i < cbs.create.length; ++i) {
              cbs.create[i](空のノード、祖先)
            }
            //#6513
            // 作成フックによってマージされた可能性のある挿入フックを呼び出します。
            // たとえば、「挿入」フックを使用するディレクティブの場合。
            const 挿入 = ancestor.data.hook.insert
            if (挿入.マージ) {
              // コンポーネントマウントフックの再呼び出しを避けるためにインデックス1から開始します
              (i = 1 とします; i < insert.fns.length; i++) {
                挿入.fns[i]()
              }
            }
          } それ以外 {
            registerRef(祖先)
          }
          祖先 = 祖先.親
        }
      }

      // 古いノードを破棄する
      if (isDef(parentElm)) {
        /*古いノードを削除*/
        削除Vnodes([古いVnode], 0, 0)
      } そうでない場合 (isDef(oldVnode.tag)) {
        /*破棄フックを呼び出す*/
        呼び出しDestroyHook(古いVnode)
      }
    }
  }

  /*挿入フックを呼び出す*/
  挿入フックを呼び出します(vnode、挿入されたVnodeQueue、isInitialPatch)
  vnode.elmを返す
}

Vue の diff アルゴリズムは同じレイヤー上のノードを比較するため、時間の計算量は O(n) のみで、アルゴリズムは非常に効率的です。 コードから、古いノードと新しいノードが同じノードであるかどうかを判断するために、patch で sameVnode が使用されていることもわかります。同じノードである場合は、さらに patchVnode が実行され、そうでない場合は新しい DOM が作成され、古い DOM が削除されます。

同じVノード

次に、sameVnode が 2 つのノードが同じノードであるかどうかを判断する方法を見てみましょう。

/*
  2 つの VNode ノードが同じノードであるかどうかを判断するには、次の条件を満たす必要があります。キーが同じである、タグ (現在のノードのラベル名) が同じである、isComment (コメント ノードであるかどうか) が同じである、データ (現在のノードに対応するオブジェクトで、特定のデータ情報が含まれている、VNodeData 型である、VNodeData 型でデータ情報を参照できる) が定義されているかどうか、タグが <input> の場合、型が同じである必要があります*/
関数sameVnode(a, b) {
  戻る (
    a.キー === b.キー && (
      (
        a.タグ === b.タグ &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        同じ入力タイプ(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

// 一部のブラウザは <input> の動的変更をサポートしていません
// したがって、それらは異なるノードとして扱う必要がある
/*
  タグが <input> の場合、タイプが同じかどうかを判断します。一部のブラウザは <input> タイプの動的な変更をサポートしていないため、異なるタイプとみなされます*/
関数sameInputType(a, b) {
  if (a.tag !== 'input') は true を返します
  私は
  定数 typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  定数 typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  戻り値 typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

sameVnode は、キー、タグ、コメントノード、データ情報を比較して、2 つの Node ノードが同じかどうかを判断します。入力タグは、異なるブラウザと互換性があるように個別に判断されます。

パッチVノード

 // diffアルゴリズムはノードを比較します function patchVnode (
  古いVノード、
  vノード、
  挿入されたVnodeQueue、
  所有者配列、
  索引、
  削除のみ
){
  /* 2 つの VNode ノードが同じ場合は直接返します */
  (oldVnode === vnode)の場合{
    戻る
  }

  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // 再利用された vnode を複製する
    vnode = ownerArray[インデックス] = cloneVNode(vnode)
  }

  const elm = vnode.elm = 古いVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    (vnode.asyncFactory.resolved がisDefである)場合
      水和物(古いVnode.elm、vnode、挿入されたVnodeQueue)
    } それ以外 {
      vnode.isAsyncPlaceholder = true
    }
    戻る
  }

  // 静的ツリーの要素を再利用します。
  // vnode が複製された場合にのみこれを実行することに注意してください -
  // 新しいノードが複製されていない場合は、レンダリング関数が
  // ホットリロード API によってリセットされ、適切な再レンダリングを行う必要があります。
  /*
    古いVNodeと新しいVNodeの両方が静的であり、キーが同じ(同じノードを表す)場合、
    そして、新しいVNodeは複製されるか、一度だけマークされます(v-once属性でマークされ、一度だけレンダリングされます)。
    次に、elm と componentInstance を置き換えるだけです。
  */
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === 古いVnode.key &&
    (vnode.isCloned が True の場合 || vnode.isOnce が True の場合)
  ){
    vnode.componentInstance = 古いVnode.componentInstance
    戻る
  }

  // いくつかのコンポーネントフックを実行します/*data.hook.prepatch が存在する場合は、最初にそれを実行します*/
  私は
  定数データ = vnode.data
  isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch) の場合
    /*i = data.hook.prepatch、存在する場合は、「./create-component componentVNodeHooks」を参照してください。 */
    i(古いVノード、vノード)
  }

  // 古いノードと新しいノードに子があるかどうかを確認します const oldCh = oldVnode.children
  定数ch = vnode.children

  // プロパティ更新 if (isDef(data) && isPatchable(vnode)) {
    // cbs 内の属性更新の配列を取り出します [attrFn, classFn,​​ ​​...]
    /*更新コールバックと更新フックを呼び出す*/
    (i = 0; i < cbs.update.length; ++i) の場合、cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }

  // 要素かどうかを判定する if (isUndef(vnode.text)) { /*このVNodeノードにテキストがない場合*/
    // 双方に子供がいる if (isDef(oldCh) && isDef(ch)) {
      /*古いノードと新しいノードの両方に子ノードがある場合は、子ノードに対して diff 操作を実行し、updateChildren を呼び出します*/
      oldCh !== ch の場合、elm の子を更新します。oldCh の子は ch の子であり、insertedVnodeQueue の子は ch の子であり、removeOnly の子は elm の子です。
    } そうでなければ (isDef(ch)) {
      /*古いノードに子ノードがなく、新しいノードに子ノードがある場合は、まずelmのテキストコンテンツをクリアし、次に現在のノードに子ノードを追加します*/
      process.env.NODE_ENV !== 'production' の場合 {
        重複キーのチェック(ch)
      }
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, 挿入されたVnodeQueue)
    } そうでなければ (isDef(oldCh)) {
      /*新しいノードに子ノードがなく、古いノードに子ノードがある場合は、ele のすべての子ノードを削除します*/
      Vnodesを削除します(oldCh, 0, oldCh.length - 1)
    } そうでない場合 (isDef(oldVnode.text)) {
      /*新しいノードと古いノードの両方に子ノードがない場合、テキストのみが置き換えられます。このロジックでは新しいノードのテキストが存在しないため、ele のテキストは直接削除されます*/
      nodeOps.setTextContent(elm, '')
    }
  } そうでない場合 (oldVnode.text !== vnode.text) {
    /*新しいノードと古いノードのテキストが異なる場合は、このテキストを直接置き換えます*/
    nodeOps.setTextContent(elm、vnode.text) は、
  }
  /*ポストパッチフックを呼び出す*/
  if (isDef(データ)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

patchVnode のプロセスは次のとおりです。

  1. oldVnode と Vnode が同じオブジェクトである場合、それらは更新されずに直接返されます。
  2. 古い VNode と新しい VNode が両方とも静的で、キーが同じ (同じノードを表す) であり、新しい VNode が複製されるか once でマークされる (v-once 属性でマークされ、1 回だけレンダリングされる) 場合は、elm と componentInstance を置き換えるだけで済みます。
  3. vnode.text がテキスト ノードではなく、古いノードと新しいノードの両方に子ノードがあり、古いノードと新しいノードの子ノードが異なる場合は、子ノードに対して diff 操作が実行され、diff の中核でもある updateChildren が呼び出されます。
  4. 古いノードに子ノードがなく、新しいノードに子ノードがある場合は、まず古いノードDOMのテキストコンテンツをクリアし、次に現在のDOMノードに子ノードを追加します。
  5. 新しいノードに子ノードがなく、古いノードに子ノードがある場合、DOM ノードのすべての子ノードが削除されます。
  6. 新しいノードと古いノードの両方に子ノードがない場合、テキストのみが置き換えられます。

更新子供

ページの DOM はツリー構造です。上記の patchVnode メソッドは同じ DOM 要素を再利用します。新しい VNnode オブジェクトと古い VNnode オブジェクトの両方に子要素がある場合、再利用された要素をどのように比較すればよいでしょうか。これが updateChildren メソッドが行うことです。

/*
  diff コアメソッド、比較最適化*/
関数 updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  oldStartIdx = 0 とする
  newStartIdx = 0 とします
  oldEndIdx = oldCh.length - 1 とします。
  oldStartVnode = oldCh[0]とする
  oldEndVnode = oldCh[oldEndIdx]とする
  newEndIdx = newCh.length - 1 とします。
  newStartVnode = newCh[0]とする
  newEndVnode = newCh[newEndIdx]とします。
  oldKeyToIdx、idxInOld、vnodeToMove、refElm を使用します。

  // removeOnly は <transition-group> でのみ使用される特別なフラグです
  // 削除された要素が正しい相対位置に留まるようにする
  // 退出遷移中
  const canMove = !removeOnly

  process.env.NODE_ENV !== 'production' の場合 {
    重複キーのチェック(newCh)
  }

  (古い開始Idx <= 古い終了Idx && 新しい開始Idx <= 新しい終了Idx) {
    if (isUndef(oldStartVnode)) {
      /*右へ移動*/
      oldStartVnode = oldCh[++oldStartIdx] // Vnodeが左に移動されました
    } そうでない場合 (isUndef(oldEndVnode)) {
      /*左へ移動*/
      oldEndVnode = oldCh[--oldEndIdx]
    } そうでない場合 (sameVnode(oldStartVnode, newStartVnode)) {
      /*最初の 4 つのケースは、実際にはキーが指定されている場合で、同じ VNode であると判断された場合は、Vnode を直接パッチし、それぞれ oldCh と newCh の 2 つのヘッド ノードを比較します (2*2=4 ケース)*/
      パッチVnode(古い開始Vnode、新しい開始Vnode、挿入されたVnodeQueue、新しいCh、新しい開始Idx)
      古い開始Vノード = 古いCh[++古い開始Idx]
      新しい開始Vノード = 新しいCh[++新しい開始Idx]
    } そうでない場合 (sameVnode(oldEndVnode, newEndVnode)) {
      パッチVnode(古い終了Vnode、新しい終了Vnode、挿入されたVnodeQueue、新しいCh、新しい終了Idx)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnodeが右に移動
      パッチVnode(古い開始Vnode、新しい終了Vnode、挿入されたVnodeQueue、新しいCh、新しい終了Idx)
      canMove && nodeOps.insertBefore(親Elm、oldStartVnode.elm、nodeOps.nextSibling(oldEndVnode.elm))
      古い開始Vノード = 古いCh[++古い開始Idx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnodeが左に移動
      パッチVnode(古い終了Vnode、新しい開始Vnode、挿入されたVnodeQueue、新しいCh、新しい開始Idx)
      canMove && nodeOps.insertBefore(親Elm、古い終了Vnode.elm、古い開始Vnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      新しい開始Vノード = 新しいCh[++新しい開始Idx]
    } それ以外 {
      /*
        古い VNode のキーに対応するキーを持つハッシュ テーブルを生成します (undefined が初めて入ったときにのみ生成され、後で重複するキー値を検出するための道も開かれます)
        たとえば、childre は次のようになります [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}] beginIdx = 0 endIdx = 2  
        結果は{key0: 0、key1: 1、key2: 2}です。
      */
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

      /*新しい VNode ノード newStartVnode にキーがあり、このキーが oldVnode で見つかる場合は、このノードの idxInOld (つまり、ノード番号、添え字) を返します*/
      idxInOld = isDef(newStartVnode.key)
        ? 古いキーからIdx[新しい開始Vノード.キー]
        : findIdxInOld(新しい開始ノード、古いCh、古い開始Idx、古い終了Idx)
      if (isUndef(idxInOld)) { // 新しい要素
        /*NewStartVnode にキーがないか、古いノードにキーが見つからないため、新しいノードを作成します*/
        createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newStartIdx)
      } それ以外 {
        /*同じキーを持つ古いノードを取得します*/
        vnodeToMove = 古いCh[idxInOld]
        同じVnode(vnodeToMove, newStartVnode)の場合
          /*新しい VNode と同じキーを持つ取得されたノードが同じ VNode である場合は、Vnode にパッチを適用します*/
          パッチVnode(vnodeToMove、newStartVnode、insertedVnodeQueue、newCh、newStartIdx)
          /* patchVnode が入力されているため、古いノードには undefined が割り当てられます。このノードと同じキーを持つ新しいノードがある場合は、重複キーがあることが検出され、プロンプトが表示されます*/
          oldCh[idxInOld] = 未定義
          /*canMoveフラグがある場合、oldStartVnodeに対応する実際のDomノードの前に直接挿入できます*/
          canMove && nodeOps.insertBefore(親Elm、vnodeToMove.elm、oldStartVnode.elm)
        } それ以外 {
          // 同じキーだが要素が異なる。新しい要素として扱う
          /*新しい VNode が同じキーを持つ VNode と同じ VNode でない場合 (たとえば、タグが異なるか、入力タグのタイプが異なる場合)、新しいノードを作成します */
          createElm(newStartVnode、insertedVnodeQueue、parentElm、oldStartVnode.elm、false、newCh、newStartIdx)
        }
      }
      新しい開始Vノード = 新しいCh[++新しい開始Idx]
    }
  }
  (古い開始ID>古い終了ID)の場合{
    /*すべての比較が完了した後、oldStartIdx > oldEndIdx の場合、古いノードが走査され、新しいノードが古いノードよりも多いことを意味します。そのため、追加の新しいノードを 1 つずつ作成して、実際の Dom に追加する必要があります */
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    Vnodesを追加します(parentElm、refElm、newCh、newStartIdx、newEndIdx、insertedVnodeQueue)
  } そうでない場合 (newStartIdx > newEndIdx) {
    /*すべての比較が完了した後、newStartIdx > newEndIdx の場合、新しいノードがトラバースされ、新しいノードよりも古いノードの数が多いことを意味します。この時点で、冗長な古いノードを実際の Dom から削除する必要があります。*/
    Vnodes を削除します (古い Ch、古い開始 ID、古い終了 ID)
  }
}

以下は、よく書かれていると思う他のブロガーの記事のコピーです: github.com/liutao/vue2…


一見すると、このコードブロックは少しわかりにくいかもしれません。具体的な内容は実はそれほど複雑ではありません。まずは判断プロセス全体を大まかに見ていき、その後、いくつかの例を通して詳細に見ていきましょう。

oldStartIdx、newStartIdx、oldEndIdx、newEndIdx はすべてポインタです。それぞれが何を指しているのかは皆さんご存じだと思います。比較プロセス全体を通して、ポインタを移動し続けます。

oldStartVnode、newStartVnode、oldEndVnode、および newEndVnode は、上記のポインターに 1 対 1 で対応し、それらが指す VNode ノードです。

while ループは、oldCh または newCh のトラバースが完了すると停止します。それ以外の場合は、ループ プロセスは実行を継続します。プロセス全体は、次の状況に分かれています。

1. oldStartVnode が定義されていない場合、oldCh 配列トラバーサルの開始ポインターは 1 つ前の位置に移動します。

  if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnodeが左に移動されました
  }

注: 7番目のケースを参照してください。同じキー値が未定義に設定されている可能性があります。

2. oldEndVnode が定義されていない場合、oldCh 配列トラバーサルの開始ポインターは 1 つ前方に移動します。

  そうでない場合 (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
  } 

注: 7番目のケースを参照してください。同じキー値が未定義に設定されている可能性があります。

3. sameVnode(oldStartVnode, newStartVnode)、ここでは、2 つの配列開始ポインターによって指されるオブジェクトを再利用できるかどうかが判断されます。 true を返す場合、まず patchVnode メソッドが呼び出され、DOM 要素が再利用され、子要素が再帰的に比較され、次に oldCh と newCh の開始ポインターがそれぞれ 1 つ前に戻ります。

  そうでない場合 (sameVnode(oldStartVnode, newStartVnode)) {
    パッチVnode(古い開始Vnode、新しい開始Vnode、挿入されたVnodeQueue)
    古い開始Vノード = 古いCh[++古い開始Idx]
    新しい開始Vノード = 新しいCh[++新しい開始Idx]
  }

4. sameVnode(oldEndVnode, newEndVnode)、ここでは、2 つの配列終了ポインターによって指されるオブジェクトを再利用できるかどうかが判断されます。 true が返された場合、最初に patchVnode メソッドが呼び出され、DOM 要素が再利用され、子要素が再帰的に比較され、次に oldCh と newCh の終了ポインタがそれぞれ 1 つ前方に移動します。

  そうでない場合 (sameVnode(oldEndVnode, newEndVnode)) {
    パッチVnode(古いEndVnode、新しいEndVnode、挿入されたVnodeQueue)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
  } 

5. sameVnode(oldStartVnode, newEndVnode)、ここでは、oldCh 開始ポインターが指すオブジェクトと newCh 終了ポインターが指すオブジェクトを再利用できるかどうかが判断されます。 true を返す場合は、まず patchVnode メソッドが呼び出され、DOM 要素を再利用して子要素を再帰的に比較します。再利用される要素は newCh の終了ポインターが指す要素であるため、oldEndVnode.elm の前に挿入されます。最後に、oldCh の開始ポインターは 1 つ後ろの位置に移動し、newCh の開始ポインターは 1 つ前の位置に移動します。

  else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnodeが右に移動
    パッチVnode(古い開始Vnode、新しい終了Vnode、挿入されたVnodeQueue)
    canMove && nodeOps.insertBefore(親Elm、oldStartVnode.elm、nodeOps.nextSibling(oldEndVnode.elm))
    古い開始Vノード = 古いCh[++古い開始Idx]
    newEndVnode = newCh[--newEndIdx]
  }

6. sameVnode(oldEndVnode, newStartVnode)、ここでは、oldCh の終了ポインターが指すオブジェクトと newCh の開始ポインターが指すオブジェクトを再利用できるかどうかが判断されます。 true を返す場合は、まず patchVnode メソッドが呼び出され、DOM 要素を再利用して子要素を再帰的に比較します。再利用される要素は newCh の開始ポインターが指す要素であるため、oldStartVnode.elm の前に挿入されます。最後に、oldCh の終了ポインターは 1 つ前に移動し、newCh の開始ポインターは 1 つ後ろに移動します。

  else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnodeが左に移動
    パッチVnode(古い終了Vnode、新しい開始Vnode、挿入されたVnodeQueue)
    canMove && nodeOps.insertBefore(親Elm、古い終了Vnode.elm、古い開始Vnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    新しい開始Vノード = 新しいCh[++新しい開始Idx]
  }

7. 上記の 6 つの条件のいずれも満たされない場合は、ここをクリックしてください。これまでの比較はすべて先頭と末尾の組み合わせの比較でした。ここでの状況は少し複雑です。実際には、主にキー値に応じた要素の再利用に基づいています。

① oldCh配列を走査し、キーを持つオブジェクトを見つけ、そのキーをキー、インデックス値を値として新しいオブジェクトoldKeyToIdxを生成します。

if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
関数createKeyToOldIdx (children, beginIdx, endIdx) {
  let i、キー
  定数マップ = {}
  (i = beginIdx; i <= endIdx; ++i) の場合 {
    キー = children[i].key
    if (isDef(key)) map[key] = i
  }
  戻る地図
}

② newStartVnodeにキー値があるか確認し、oldKeyToIdxに同じキーがあるか確認します。

  idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null

③ newStartVnode にキーがない場合、または oldKeyToIdx に同じキーがない場合は、createElm メソッドが呼び出されて新しい要素が作成され、newCh の開始インデックスが 1 つ前に戻ります。

  if (isUndef(idxInOld)) { // 新しい要素
    createElm(新しい開始Vnode、挿入されたVnodeQueue、親Elm、古い開始Vnode.elm)
    新しい開始Vノード = 新しいCh[++新しい開始Idx]
  } 

④ elmToMoveは、移動する要素を格納します。sameVnode(elmToMove, newStartVnode)がtrueを返した場合、再利用可能であることを意味します。このとき、最初にpatchVnodeメソッドが呼び出され、DOM要素を再利用し、子要素を再帰的に比較します。oldCh内の相対要素はundefinedにリセットされ、次に現在の要素がoldStartVnode.elmの前に挿入され、newChの開始インデックスが1つ前に戻ります。たとえば、タグ名が異なるなど、sameVnode(elmToMove, newStartVnode) が false を返す場合は、createElm メソッドが呼び出されて新しい要素が作成され、newCh の開始インデックスが 1 つ前に戻ります。

  elmToMove = 古いCh[idxInOld]
  同じVnode(elmToMove, newStartVnode)の場合
    パッチVnode(elmToMove、newStartVnode、insertedVnodeQueue)
    oldCh[idxInOld] = 未定義
    canMove && nodeOps.insertBefore(親Elm、新しいStartVnode.elm、古いStartVnode.elm)
    新しい開始Vノード = 新しいCh[++新しい開始Idx]
  } それ以外 {
    // 同じキーだが要素が異なる。新しい要素として扱う
    createElm(新しい開始Vnode、挿入されたVnodeQueue、親Elm、古い開始Vnode.elm)
    新しい開始Vノード = 新しいCh[++新しい開始Idx]
  }

上記は、vue diff アルゴリズムの使用に関する詳細な内容です。vue diff アルゴリズムの詳細については、123WORDPRESS.COM の他の関連記事に注目してください。

以下もご興味があるかもしれません:
  • Vue の仮想 DOM と diff アルゴリズムをご存知ですか?
  • Vueのdiffアルゴリズムについての簡単な説明
  • Vue2のdiffアルゴリズムの詳細な説明
  • vue3.0 diffアルゴリズムの使用に関する詳細な説明(超詳細)
  • Vueのdiffアルゴリズム原理の詳細な説明
  • Vue の diff アルゴリズムの原理を本当に理解していますか?

<<:  Mysql 5.7.19 無料インストール版 (64 ビット) の設定方法に関する詳細なチュートリアル

>>:  CentOS 6.5 の設定 ssh キーフリーログインで pssh コマンドを実行する方法の説明

推薦する

JSはシンプルなカウンターを実装します

HTML CSS および JavaScript を使用して、プラス、マイナス、ゼロの 3 つのボタン...

my.cnf (my.ini) 重要なパラメータの最適化設定手順

MyISAM ストレージエンジンMyISAM ストレージ エンジンは、書き込みよりも読み取りが多く、...

docker ストレージを使用して Exit を実行すると、サーバーへのファイルのアップロードが失敗する問題と解決策

1. 問題の説明Docker コンテナにインストールされているストレージが終了状態になっているため、...

QT が MYSQL データベースに接続するための詳細な手順

最初のステップは、対応するデータベースモジュール(sql)をプロジェクトファイル( .pro )に追...

MySQL トリガー: トリガーの作成と使用

この記事では、例を使用して MySQL トリガーの作成と使用について説明します。ご参考までに、詳細は...

WeChat アプレット uniapp は左スワイプによる削除効果を実現します (完全なコード)

WeChatアプレットuniappは左スワイプで削除効果を実現成果を達成する1. スワイプしてリス...

JS の難しさ 同期と非同期、スコープとクロージャ、プロトタイプとプロトタイプ チェーンの詳細な説明

目次JS スリーマウンテンズ同期 非同期同期と非同期の違い範囲、終了関数スコープチェーンブロックスコ...

MySQL にテーブルが存在するかどうかを確認し、それを一括で削除する方法

1. インターネットで長時間検索しましたが、判定表が存在するかどうかがわからなかったので、漠然と削除...

K8Sの高度な機能を理解するための記事

目次K8Sの高度な機能高度な機能要約するkubectl サービスの問題のトラブルシューティングK8S...

MySQLでテーブル名を変更する方法と注意すべき点

目次1. テーブル名を変更する方法2. 注記要約: 1. テーブル名を変更する方法RENAME TA...

Linux centos7 環境での MySQL インストール チュートリアル

Linux centos7 環境に MySQL をインストールする手順の詳細な紹介MySQLをインス...

Reactホームページの読み込みが遅い問題のパフォーマンス最適化事例の詳細な説明

しばらくReactを勉強した後、実践してみたいと思います。そこで、個人のブログのウェブサイトを再構築...

CSS3を使用してヘッダーアニメーション効果を作成する

Netease Kanyouxi公式サイト(http://kanyouxi.163.com/)(棚...

Xtrabackup を使用した MySQL バックアップ プロセスの詳細な説明

目次01 背景02 はじめに03 ワークフロー04 いくつかの質問05 ファイルをバックアップする0...

要素UIテーブルはドロップダウンフィルタリング機能を実現します

この記事の例では、要素UIテーブルにドロップダウンフィルタリングを実装するための具体的なコードを参考...