ホームページ  >  記事  >  ウェブフロントエンド  >  vue2.x の差分アルゴリズムの原理の詳細な分析

vue2.x の差分アルゴリズムの原理の詳細な分析

青灯夜游
青灯夜游転載
2022-08-15 19:54:211977ブラウズ

diff アルゴリズムは、ツリー ノードを同じレベルで比較する効率的なアルゴリズムであり、ツリーをレイヤーごとに検索して横断する必要がなくなります。この記事では、vue2.x の差分アルゴリズムの原理を詳しく分析します。お役に立てば幸いです。

vue2.x の差分アルゴリズムの原理の詳細な分析

私はソース コード分析の記事をたくさん読み、ソース コードを少なくとも 2 回読みました。やっぱり、自分自身の記録や勉強として、自分で書いていきたいと思っています。 コメントと概要に注目し、残りのことはあまり気にしないでください。概要とソース コード レビュー プロセスおよびコメントを比較すると、より多くの情報が得られます

# #更新メソッドの定義

レンダー関数生成後、マウントメソッドが呼び出され、マウント時に差分計算が行われます初期化時は古い仮想ノードが存在しないため、実際の dom ノードと仮想ノードが初めて比較されますが、比較のために、仮想ノードはネイティブ ノードではないため、初めて置き換え操作が実行されます。 (学習ビデオ共有:

vue ビデオ チュートリアル)

// /src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode // 当前render函数产生的虚拟节点,保存后以便下次做对比
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) //初次渲染
    } else {
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
   ...
  }

diff アルゴリズムの 2 つの主要なブランチ

本体には 2 つの主要なブランチがあります: 前後の仮想 ノードは一致していますが、前後の仮想ノードが一致していません

// /src/core/vdom/patch.js
export function createPatchFunction (backend) {
  ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ...
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        ...// 前后虚拟节点一致的方法
      } else {
        ...// 前后虚拟节点不一致的方法
      }
  }
}

前後の仮想ノードが一致していません

に分かれています。 3 つのステップ: 1. 新しいノードを作成する、2. 親プレースホルダー ノードを更新する、3. 古いノードを削除する

コンポーネントが初めてマウントされるとき、この 2 つは異なります。これは本物の dom です。仮想ノードに変換されて置き換えられます

if (isRealElement) {
  ...
  //需要diff 所以将第一次的真实节点转换成虚拟节点
  oldVnode = emptyNodeAt(oldVnode) //<div id="app"></div>
}
// 拿到父类的dom节点
const oldElm = oldVnode.elm //app
const parentElm = nodeOps.parentNode(oldElm) // body
//创建新dom节点 内部包含组件逻辑
createElm(
  vnode,
  insertedVnodeQueue,
  oldElm._leaveCb ? null : parentElm,
  nodeOps.nextSibling(oldElm)
)
//更新父的占位符节点 (组件更新相关)
if (isDef(vnode.parent)) {
  // 在生成render函数时会生成占位符节点<Dialog>提示</Dialog> => <div>提示</div> <Dialog></Dialog>就是占位符节点
  let ancestor = vnode.parent
  // 判断是否可挂载
  const patchable = isPatchable(vnode)
  while (ancestor) {
    for (let i = 0; i < cbs.destroy.length; ++i) {
      cbs.destroy[i](ancestor)
    }
    //更新父占位符的element
    ancestor.elm = vnode.elm
    if (patchable) {
      ...
    } else {
      registerRef(ancestor)
    }
    ancestor = ancestor.parent
  }
}
// 删除旧节点
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}

フロントとリアの仮想ノードは一貫しています

    最初に、新しいノードがテキストである場合は、テキストを直接設定します。そうでない場合は、引き続き決定します。
  • 新しいノードと古いノードには子があり、詳細な比較 (キー ポイント)
  • 新しいノードには子がありますが、古いノードには子がありません。そのため、ループ内に新しいノードを追加してください。
  • 新しいノードには子がありませんが、古いノードには子があります。古いノードを直接削除してください。
  • function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {
        const elm = vnode.elm = oldVnode.elm
    
        let i
        const data = vnode.data 
        // 是组件vnode,在组件更新会调用组件的prepatch方法
        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)
        }
        // 是否是text
        if (isUndef(vnode.text)) {
          // 新旧节点都有children
          if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
          // 新有 老没有 children 循环创建新节点
          } else if (isDef(ch)) {
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          // 新没有 老有 children 直接删除老节点
          } else if (isDef(oldCh)) {
            removeVnodes(oldCh, 0, oldCh.length - 1)
          // 新老都没有 children 老的是文本 就置为空
          } else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, &#39;&#39;)
          }
        // 是text 直接设置文本
        } 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)
        }
      }
新旧ノードと子ノードの比較

// /src/core/vdom/patch.js
 function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 老节点开始索引
    let newStartIdx = 0 // 新节点开始索引
    let oldEndIdx = oldCh.length - 1 // 老节点末尾索引
    let oldStartVnode = oldCh[0] // 老节点开始元素
    let oldEndVnode = oldCh[oldEndIdx] // 老节点末尾元素
    let newEndIdx = newCh.length - 1 // 新节点末尾索引
    let newStartVnode = newCh[0] // 新节点开始元素
    let newEndVnode = newCh[newEndIdx] // 新节点末尾元素
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    const canMove = !removeOnly
    // 满足新节点开始索引小于新节点结束索引,旧节点开始索引小于旧节点结束索引
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) { // 是否定义老节点开始元素
        oldStartVnode = oldCh[++oldStartIdx]
      } else if (isUndef(oldEndVnode)) {// 是否定义老节点结束元素
        oldEndVnode = oldCh[--oldEndIdx]
        // 头(旧节点开始元素)头(新节点开始元素)对比 例如四个li,末尾新增一个li,这种情况头头对比性能高
      } else if (sameVnode(oldStartVnode, newStartVnode)) { // sameVnode判断key和tag是否相同
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾对比 例如四个li,头部新增一个li,这种情况尾尾对比性能高
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {// 头尾对比 节点反转优化 reverse
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // 尾头对比
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else { // 乱序对比(核心diff,其他方式为优化)
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 多出来的新节点直接做插入 多出来的旧节点删除
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

注意事項

    key では、変更前後のインデックスが使用できない場合があります。はい、先頭に要素を追加する際、インデックスをキーにすると更新エラーになりますが、Vueは最後に要素を追加したものとして認識します(前後のキーが全て0なので)
  • さまざまな比較 この場合、2 つが同じであることが判明する限り、子は再帰的に比較されます
  • アウトオブオーダー比較では、キーが再生されます。素晴らしい役割です。キーがないと更新エラーが発生し、再利用効果が得られません。
  • 差分比較は
  • 深さ優先、同層比較です

#概要

マウント時、差分アルゴリズムを経た後にテンプレートが更新され、初めて実際のdomノードと生成された仮想ノードが比較され、生成された仮想ノードが後続の更新と比較のために保存されます。 diff アルゴリズムは 2 つのブランチに分割するだけで済みます。前方と後方の仮想ノードは一貫性があり、前方と後方の仮想ノードは不一致です。前後の仮想ノードに矛盾がある場合、新しいノードが作成され、親プレースホルダーが更新され、古いノードが削除されます。古いノードが実ノードの場合は、それを仮想ノードに変換し、古いノードの親ノードを取得して、古いノードを置き換えます。前と後ろの仮想ノードに一貫性がある場合、最初に新しいノードがテキストであるかどうかを判断し、値が直接追加される場合は属性を比較してから、新しいノードに子があり、古いノードに子がないかどうかを判断します。子がある場合は、新しいノードの子を直接追加します。新しいノードに子がある場合は、直接追加されます。ノードが存在せず、古いノードが存在する場合は、古いノードの子が削除されます。古いノードと子ノードの場合は、古いノードの子が削除されます。新しいノードには子があり、ダブル ポインタは、頭から頭への比較、尾から尾への比較、頭から尾への比較、尾から頭への比較、および外側からの比較を通じて、同じレイヤーを比較するために使用されます。順序比較. 古いノードを最大限に活用するために継続的に反復的に判断して更新します. 冗長な新しいノードがある場合は追加され、冗長な古いノードは削除されます. 最後に、比較された仮想ノードが返され、次回比較する古いノード。

  • 直接比較
    新しい開始要素と古い開始要素が同じ vnode である場合、深い比較のために patchVnode メソッドを再帰的に呼び出してから、インデックスを次の vnode に移動します。 element
  • 末尾同士の比較
    古い終了要素と新しい終了要素が同じ vnode である場合、詳細な比較のために patchVnode メソッドを再帰的に呼び出してから、インデックスを前の要素
  • 先頭から末尾までの比較
    古いノードの開始要素と古いノードを比較します末尾の要素を比較します。同じ場合は、patchVnode メソッドを再帰的に呼び出します。次に、古いノード要素を最後に移動し、古いノードの先頭ポインタを次のノードに移動し、新しいノードの末尾ポインタを前のノードに移動します。たとえば、古い: A、B、C、新しい: C、B、A。最初の比較では、古い A を C の後ろに移動します。
  • 末尾と先頭の比較
    末尾の要素を移動します。古いノードの先頭要素を古いノードの開始要素に移動します。比較のために、patchVnode メソッドを再帰的に呼び出して詳細な比較を行います。次に、古いノード要素を先頭に移動し、古いノードの末尾ポインタを前のノードに移動します。前のノードに移動し、新しいノードのヘッド ポインタを次のノードに移動します。たとえば、古いもの: A、B、C、新しいもの: C、B、A、D。最初の比較では、古い C が A の前に移動されます。
  • 順序外れの比較
    は比較前のキーと対応関係に基づいて、古いノードのインデックスがマッピング テーブルに生成されます。アウトオブオーダー比較中、古いノードのキーを見つけるために現在のキーが使用されます。再利用できる場合、ノードは古いノードの先頭に移動され、子が再帰的に比較されます。再利用することはできず、新しいノードが作成され、ノードの先頭に古いノードに挿入されます。次に、新しいインデックスを次の要素

(学習ビデオ共有: Web フロントエンド開発基本プログラミング ビデオ)

に移動します。

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

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。