ホームページ  >  記事  >  ウェブフロントエンド  >  vue の仮想 DOM と Diff アルゴリズムについての深い理解

vue の仮想 DOM と Diff アルゴリズムについての深い理解

青灯夜游
青灯夜游転載
2021-12-17 19:34:562106ブラウズ

この記事では、vue の仮想 DOM と Diff アルゴリズムを深く理解し、仮想 DOM と Diff アルゴリズムを詳細に分析します。

vue の仮想 DOM と Diff アルゴリズムについての深い理解

最近仮想 DOM と Diff をレビューし、多くの情報を読み、vue についての理解を深めるためにこの長い記事を要約します。

この記事では、Vue の仮想 DOM と Diff アルゴリズムをより詳細に分析します。いくつかの重要なポイントは、(グラフィックス マスターのおかげで) 他の場所からいくつかの画像を移動することによって説明されており、より詳細なソース コードの解釈も含まれています。良い記事だと思ったら高評価をお願いします 間違いがあればコメント欄でご指摘ください もちろん分からないことがあれば大歓迎です質問をする。 [関連する推奨事項: "vue.js チュートリアル "]

実際の DOM のレンダリング

仮想 DOM について話す前に、実際の DOM のレンダリングについて話しましょう。

vue の仮想 DOM と Diff アルゴリズムについての深い理解

ブラウザの実際の DOM レンダリングのプロセスは、次の部分に大別されます。

  • DOM ツリーの構築 。 HTML パーサーを通じて HTML タグを解析して処理し、DOM ツリーに構築します。パーサーは非ブロッキング リソース (画像、CSS) に遭遇すると解析を続行しますが、スクリプト タグ (特に async および defer 属性なし) に遭遇した場合は、レンダリングをブロックし、HTML 解析を停止します。そのため、script タグを本文の下に置くのが最善です。

  • CSSOM ツリーを構築します。 DOM の構築と同様に、ブラウザはスタイル ルールを CSSOM に構築します。ブラウザは CSS で設定されたルールをたどり、CSS セレクターに基づいて親子関係、兄弟関係などのノード ツリーを作成します。

  • レンダー ツリーを構築します。このステップでは、DOM と CSSOM を関連付け、各 DOM 要素にどの CSS ルールを適用するかを決定します。関連するすべてのスタイルを DOM ツリー内の各表示ノードに照合し、CSS カスケードに基づいて各ノードの計算されたスタイルを決定します。非表示のノード (head、display:none を含む属性を持つノード) は Render ツリーに生成されません。

  • レイアウト/リフロー。ブラウザが初めてノードの位置とサイズを決定することをレイアウトと呼びます。後で ノードの位置とサイズが変更された場合、このステップによりレイアウト調整 (リフロー ) がトリガーされます。

  • ペイント/再ペイント。テキスト、色、境界線、影、およびボタンや画像などの置換された要素を含む、要素のすべての表示部分を画面に描画します。 テキスト、色、境界線、影およびその他の要素が変更されると、Repaintがトリガーされます。再描画が最初の描画よりも早く行われるようにするために、多くの場合、画面上の描画は複数のレイヤーに分割されます。コンテンツを GPU レイヤーにプロモートすると (変換、フィルター、変更、不透明度によってトリガーできます)、描画と再描画のパフォーマンスを向上させることができます。

  • 合成。この手順では、描画プロセスでレイヤーを結合し、正しいコンテンツが画面上に表示されるように正しい順序で描画されるようにします。

仮想 DOM が必要な理由

上記は DOM のレンダリング プロセスです。DOM が更新された場合は、一度 DOM を再レンダリングする必要があります。以下の場合状況が存在します

<body>
    <div id="container">
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
				....
        <div class="content" style="color: red;font-size:16px;">
            This is a container
        </div>
    </div>
</body>
<script>
    let content = document.getElementsByClassName(&#39;content&#39;);
    for (let i = 0; i < 1000000; i++) {
        content[i].innerHTML = `This is a content${i}`;
        // 触发回流
        content[i].style.fontSize = `20px`;
    }
</script>

次に、実際に DOM を 100 万回操作し、100 万回リフローをトリガーする必要があります。すべての DOM 更新は、実際の DOM を更新するプロセスに従い、違いはありません。そのため、パフォーマンスに多くの無駄が発生しました。ループ内にリフローや再描画を頻繁にトリガーする複雑な操作がある場合、パフォーマンスに影響を及ぼしやすく、遅延が発生します。なお、ここで注意すべき点は、仮想 DOM だからといって DOM より速いというわけではなく、パフォーマンスはシナリオに依存し、仮想 DOM のパフォーマンスはテンプレートのサイズに正の相関があることです。仮想 DOM の比較プロセスでは、データのサイズが区別されません。コンポーネント内に動的ノードが少数しかない場合でも、仮想 DOM は vdom 全体をトラバースします。これは、直接レンダリングと比較して追加の操作層になります。

<div class="list">
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">item</p>
    <p class="item">{{ item }}</p>
    <p class="item">item</p>
    <p class="item">item</p>
  </div>

たとえば、上記の例では、仮想 DOM です。動的ノードは 1 つだけですが、仮想 DOM は差分リスト全体のクラス、テキスト、ラベル、その他の情報をトラバースする必要があり、最終的には DOM レンダリングが必要です。 DOM 操作だけであれば、特定の DOM を操作してレンダリングするだけで済みます。仮想 DOM の中心的な価値は、より表現力豊かな js を通じて実際の DOM を記述できることです。宣言型言語操作を通じて、ほとんどのシナリオで手動による最適化を行わずに、より便利で高速な開発エクスペリエンスを開発者に提供します。パフォーマンスの限界が保証され、価格性能比が高くなります。

仮想 DOM

仮想 DOM は本質的に、オブジェクトを通じて実際の DOM 構造を表す js オブジェクトです。 Tag はタグを記述するために使用され、props は属性を記述するために使用され、子はネストされた階層関係を表すために使用されます。

const vnode = {
    tag: &#39;div&#39;,
    props: {
        id: &#39;container&#39;,
    },
    children: [{
        tag: &#39;div&#39;,
        props: {
            class: &#39;content&#39;,
        },
      	text: &#39;This is a container&#39;
    }]
}

//对应的真实DOM结构
<div id="container">
  <div class="content">
    This is a container
  </div>
</div>

仮想 DOM の更新では、DOM はすぐには操作されません。代わりに、diff アルゴリズムを使用して更新が必要なノードを見つけ、必要に応じてノードを更新し、更新されたコンテンツをファイルとして保存します。 js オブジェクト. 更新が完了したら、実際の DOM にマウントして、実際の DOM 更新を実現します。仮想 DOM により、実際の DOM を操作する際の 3 つの問題が解決されます。

  • 无差别频繁更新导致DOM频繁更新,造成性能问题

  • 频繁回流与重绘

  • 开发体验

另外由于虚拟DOM保存的是js对象,天然的具有跨平台的能力,而不仅仅局限于浏览器。

优点

总结起来,虚拟DOM的优势有以下几点

  • 小修改无需频繁更新DOM,框架的diff算法会自动比较,分析出需要更新的节点,按需更新

  • 更新数据不会造成频繁的回流与重绘

  • 表达力更强,数据更新更加方便

  • 保存的是js对象,具备跨平台能力

不足

虚拟DOM同样也有缺点,首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。

虚拟DOM实现原理

主要分三部分

  • 通过js建立节点描述对象

  • diff算法比较分析新旧两个虚拟DOM差异

  • 将差异patch到真实dom上实现更新

Diff算法

为了避免不必要的渲染,按需更新,虚拟DOM会采用Diff算法进行虚拟DOM节点比较,比较节点差异,从而确定需要更新的节点,再进行渲染。vue采用的是深度优先,同层比较的策略。

vue の仮想 DOM と Diff アルゴリズムについての深い理解

新节点与旧节点的比较主要是围绕三件事来达到渲染目的

  • 创建新节点

  • 删除废节点

  • 更新已有节点

如何比较新旧节点是否一致呢?

function sameVnode(a, b) {
    return (
        a.key === b.key &&
        a.asyncFactory === b.asyncFactory && (
            (
                a.tag === b.tag &&
                a.isComment === b.isComment &&
                isDef(a.data) === isDef(b.data) &&
                sameInputType(a, b) //对input节点的处理
            ) || (
                isTrue(a.isAsyncPlaceholder) &&
                isUndef(b.asyncFactory.error)
            )
        )
    )
}

//判断两个节点是否是同一种 input 输入类型
function sameInputType(a, b) {
    if (a.tag !== &#39;input&#39;) return true
    let i
    const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
    const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
    //input type 相同或者两个type都是text
    return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

可以看到,两个节点是否相同是需要比较标签(tag)属性(在vue中是用data表示vnode中的属性props), 注释节点(isComment) 的,另外碰到input的话,是会做特殊处理的。

创建新节点

当新节点有的,旧节点没有,这就意味着这是全新的内容节点。只有元素节点,文本节点,注释节点才能被创建插入到DOM中。

删除旧节点

当旧节点有,而新节点没有,那就意味着,新节点放弃了旧节点的一部分。删除节点会连带的删除旧节点的子节点。

更新节点

新的节点与旧的的节点都有,那么一切以新的为准,更新旧节点。如何判断是否需要更新节点呢?

  • 判断新节点与旧节点是否完全一致,一样的话就不需要更新
  // 判断vnode与oldVnode是否完全一样
  if (oldVnode === vnode) {
    return;
  }
  • 判断新节点与旧节点是否是静态节点,key是否一样,是否是克隆节点(如果不是克隆节点,那么意味着渲染函数被重置了,这个时候需要重新渲染)或者是否设置了once属性,满足条件的话替换componentInstance
  // 是否是静态节点,key是否一样,是否是克隆节点或者是否设置了once属性
  if (
    isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return;
  }
  • 判断新节点是否有文本(通过text属性判断),如果有文本那么需要比较同层级旧节点,如果旧节点文本不同于新节点文本,那么采用新的文本内容。如果新节点没有文本,那么后面需要对子节点的相关情况进行判断
//判断新节点是否有文本
if (isUndef(vnode.text)) {
  //如果没有文本,处理子节点的相关代码
  ....
} else if (oldVnode.text !== vnode.text) {
  //新节点文本替换旧节点文本
  nodeOps.setTextContent(elm, vnode.text)
}
  • 判断新节点与旧节点的子节点相关状况。这里又能分为4种情况
    • 新节点与旧节点都有子节点

    • 只有新节点有子节点

    • 只有旧节点有子节点

    • 新节点与旧节点都没有子节点

都有子节点

对于都有子节点的情况,需要对新旧节点做比较,如果他们不相同,那么需要进行diff操作,在vue中这里就是updateChildren方法,后面会详细再讲,子节点的比较主要是双端比较。

//判断新节点是否有文本
if (isUndef(vnode.text)) {
    //新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法
    if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    }
} else if (oldVnode.text !== vnode.text) {
    //新节点文本替换旧节点文本
    nodeOps.setTextContent(elm, vnode.text)
}

只有新节点有子节点

只有新节点有子节点,那么就代表着这是新增的内容,那么就是新增一个子节点到DOM,新增之前还会做一个重复key的检测,并做出提醒,同时还要考虑,旧节点如果只是一个文本节点,没有子节点的情况,这种情况下就需要清空旧节点的文本内容。

//只有新节点有子节点
if (isDef(ch)) {
  //检查重复key
  if (process.env.NODE_ENV !== &#39;production&#39;) {
    checkDuplicateKeys(ch)
  }
  //清除旧节点文本
  if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
  //添加新节点
  addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}

//检查重复key
function checkDuplicateKeys(children) {
  const seenKeys = {}
  for (let i = 0; i < children.length; i++) {
      const vnode = children[i]
      //子节点每一个Key
      const key = vnode.key
      if (isDef(key)) {
          if (seenKeys[key]) {
              warn(
                  `Duplicate keys detected: &#39;${key}&#39;. This may cause an update error.`,
                  vnode.context
              )
          } else {
              seenKeys[key] = true
          }
      }
  }
}

只有旧节点有子节点

只有旧节点有,那就说明,新节点抛弃了旧节点的子节点,所以需要删除旧节点的子节点

if (isDef(oldCh)) {
  //删除旧节点
  removeVnodes(oldCh, 0, oldCh.length - 1)
}

都没有子节点

这个时候需要对旧节点文本进行判断,看旧节点是否有文本,如果有就清空

if (isDef(oldVnode.text)) {
  //清空
  nodeOps.setTextContent(elm, &#39;&#39;)
}

整体的逻辑代码如下

function patchVnode(
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
) {
    // 判断vnode与oldVnode是否完全一样
    if (oldVnode === vnode) {
        return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
        // 克隆重用节点
        vnode = ownerArray[index] = cloneVNode(vnode)
    }

    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是否一样,是否是克隆节点或者是否设置了once属性
    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)) {
      	//调用update回调以及update钩子
        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)
    }
    //判断新节点是否有文本
    if (isUndef(vnode.text)) {
      	//新旧节点都有子节点情况下,如果新旧子节点不相同,那么进行子节点的比较,就是updateChildren方法
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } else if (isDef(ch)) {
          	//只有新节点有子节点
            if (process.env.NODE_ENV !== &#39;production&#39;) {
              	//重复Key检测
                checkDuplicateKeys(ch)
            }
          	//清除旧节点文本
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, &#39;&#39;)
          	//添加新节点
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        } else if (isDef(oldCh)) {
          	//只有旧节点有子节点,删除旧节点
            removeVnodes(oldCh, 0, oldCh.length - 1)
        } else if (isDef(oldVnode.text)) {
          	//新旧节点都无子节点
            nodeOps.setTextContent(elm, &#39;&#39;)
        }
    } 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)
    }
}

配上流程图会更清晰点

vue の仮想 DOM と Diff アルゴリズムについての深い理解

vue の仮想 DOM と Diff アルゴリズムについての深い理解

子节点的比较更新updateChildren

新旧节点都有子节点的情况下,这个时候是需要调用updateChildren方法来比较更新子节点的。所以在数据上,新旧节点子节点,就保存为了两个数组。

const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];

子节点更新采用的是双端比较的策略,什么是双端比较呢,就是新旧节点比较是通过互相比较首尾元素(存在4种比较),然后向中间靠拢比较(newStartIdx,与oldStartIdx递增,newEndIdx与oldEndIdx递减)的策略。

比较过程

vue の仮想 DOM と Diff アルゴリズムについての深い理解

向中间靠拢

vue の仮想 DOM と Diff アルゴリズムについての深い理解

这里对上面出现的新前,新后,旧前,旧后做一下说明

  • 新前,指的是新节点未处理的子节点数组中的第一个元素,对应到vue源码中的newStartVnode

  • 新后,指的是新节点未处理的子节点数组中的最后一个元素,对应到vue源码中的newEndVnode

  • 旧前,指的是旧节点未处理的子节点数组中的第一个元素,对应到vue源码中的oldStartVnode

  • 旧后,指的是旧节点未处理的子节点数组中的最后一个元素,对应到vue源码中的oldEndVnode

子节点比较过程

接下来对上面的比较过程以及比较后做的操作做下说明

  • 新前与旧前的比较,如果他们相同,那么进行上面说到的patchVnode更新操作,然后新旧节点各向后一步,进行第二项的比较...直到遇到不同才会换种比较方式

vue の仮想 DOM と Diff アルゴリズムについての深い理解

if (sameVnode(oldStartVnode, newStartVnode)) {
  // 更新子节点
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  // 新旧各向后一步
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}
  • 新后与旧后的比较,如果他们相同,同样进行pathchVnode更新,然后新旧各向前一步,进行前一项的比较...直到遇到不同,才会换比较方式

vue の仮想 DOM と Diff アルゴリズムについての深い理解

if (sameVnode(oldEndVnode, newEndVnode)) {
    //更新子节点
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // 新旧向前
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
}
  • 新后与旧前的比较,如果它们相同,就进行更新操作,然后将旧前移动到所有未处理旧节点数组最后面,使旧前与新后位置保持一致,然后双方一起向中间靠拢,新向前,旧向后。如果不同会继续切换比较方式

vue の仮想 DOM と Diff アルゴリズムについての深い理解

if (sameVnode(oldStartVnode, newEndVnode)) {
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  //将旧子节点数组第一个子节点移动插入到最后
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  //旧向后
  oldStartVnode = oldCh[++oldStartIdx]
  //新向前
  newEndVnode = newCh[--newEndIdx]
  • 新前与旧后的比较,如果他们相同,就进行更新,然后将旧后移动到所有未处理旧节点数组最前面,是旧后与新前位置保持一致,,然后新向后,旧向前,继续向中间靠拢。继续比较剩余的节点。如果不同,就使用传统的循环遍历查找。

vue の仮想 DOM と Diff アルゴリズムについての深い理解

if (sameVnode(oldEndVnode, newStartVnode)) {
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  //将旧后移动插入到最前
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  //旧向前
  oldEndVnode = oldCh[--oldEndIdx]
  //新向后
  newStartVnode = newCh[++newStartIdx]
}
  • 循环遍历查找,上面四种都没找到的情况下,会通过key去查找匹配。

进行到这一步对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}

// 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

然后拿新节点的key与旧节点进行比较,找到key值匹配的节点的位置,这里需要注意的是,如果新节点也没key,那么就会执行findIdxInOld方法,从头到尾遍历匹配旧节点

//通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找
idxInOld = isDef(newStartVnode.key)
  ? oldKeyToIdx[newStartVnode.key]
  : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

//findIdxInOld方法
function findIdxInOld(node, oldCh, start, end) {
  for (let i = start; i < end; i++) {
    const c = oldCh[i]
    //找到相同节点下标
    if (isDef(c) && sameVnode(node, c)) return i
  }
}

如果通过上面的方法,依旧没找到新节点与旧节点匹配的下标,那就说明这个节点是新节点,那就执行新增的操作。

//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作
if (isUndef(idxInOld)) {
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}

如果找到了,那么说明在旧节点中找到了key值一样,或者节点和key都一样的旧节点。如果节点一样,那么在patchVnode之后,需要将旧节点移动到所有未处理节点之前,对于key一样,元素不同的节点,将其认为是新节点,执行新增操作。执行完成后,新节点向后一步。

1vue の仮想 DOM と Diff アルゴリズムについての深い理解

//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作
if (isUndef(idxInOld)) {
  // 新增元素
  createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
  // 在旧节点中找到了key值一样的节点
  vnodeToMove = oldCh[idxInOld]
  if (sameVnode(vnodeToMove, newStartVnode)) {
    // 相同子节点更新操作
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    // 更新完将旧节点赋值undefined
    oldCh[idxInOld] = undefined
    //将旧节点移动到所有未处理节点之前
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  } else {
    // 如果是相同的key,不同的元素,当做新节点,执行创建操作
    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)
}

子节点比较总结

上面就是子节点比较更新的一个完整过程,这是完整的逻辑代码

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

    // 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 !== &#39;production&#39;) {
        checkDuplicateKeys(newCh)
    }

    //双端比较遍历
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            //旧前向后移动
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
        } else if (isUndef(oldEndVnode)) {
            // 旧后向前移动
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (sameVnode(oldStartVnode, newStartVnode)) {
            //新前与旧前
            //更新子节点
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                // 新旧各向后一步
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
            //新后与旧后
            //更新子节点
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
                //新旧各向前一步
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // 新后与旧前
            //更新子节点
            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 {
            // 对于没有设置key的节点,第一次会通过createKeyToOldIdx建立key与index的映射 {key:index}
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

            //通过新节点的key,找到新节点在旧节点中所在的位置下标,如果没有设置key,会执行遍历操作寻找
            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 {
                // 在旧节点中找到了key值一样的节点
                vnodeToMove = oldCh[idxInOld]
                if (sameVnode(vnodeToMove, newStartVnode)) {
                    // 相同子节点更新操作
                    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                        // 更新完将旧节点赋值undefined
                    oldCh[idxInOld] = undefined
                        //将旧节点移动到所有未处理节点之前
                    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
                } else {
                    // 如果是相同的key,不同的元素,当做新节点,执行创建操作
                    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)
    }
}

更多编程相关知识,请访问:编程入门!!

以上がvue の仮想 DOM と Diff アルゴリズムについての深い理解の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

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