Heim > Web-Frontend > View.js > Hauptteil

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

青灯夜游
Freigeben: 2021-12-17 19:34:56
nach vorne
2019 Leute haben es durchsucht

Dieser Artikel vermittelt Ihnen ein tiefgreifendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue und analysiert den virtuellen DOM- und Diff-Algorithmus im Detail. Ich hoffe, dass er für alle hilfreich ist.

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

Kürzlich habe ich virtuelles DOM und Diff rezensiert, viele Informationen gelesen und hiermit diesen langen Artikel zusammengefasst, um mein Verständnis von Vue zu vertiefen.

Dieser Artikel analysiert den virtuellen DOM- und Diff-Algorithmus von Vue detaillierter. Einige wichtige Punkte werden durch das Verschieben einiger Diagramme von anderswo veranschaulicht (dank des Grafikmasters), und er enthält auch eine detailliertere Interpretation des Quellcodes. Wenn Sie meinen, dass der Artikel gut ist, geben Sie mir bitte einen Daumen nach oben. Wenn Sie Fehler haben, können Sie diese natürlich gerne im Kommentarbereich angeben Fragen stellen. [Verwandte Empfehlung: „vue.js Tutorial“]

Rendering von echtem DOM

Bevor wir über virtuelles DOM sprechen, sprechen wir über das Rendering von echtem DOM.

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

Der Prozess des echten DOM-Renderings durch den Browser ist grob in die folgenden Teile unterteilt

  • Aufbau des DOM-Baums. Analysieren und verarbeiten Sie HTML-Tags über den HTML-Parser und bauen Sie sie in DOM-Bäume ein. Wenn der Parser auf nicht blockierende Ressourcen (Bilder, CSS) stößt, wird er mit der Analyse fortfahren. Wenn er jedoch auf Skript-Tags stößt (insbesondere ohne Async- und Defer-Attribute), blockiert das Rendern und stoppt das HTML-Parsen, weshalb es am besten ist, das Skript-Tag unter dem Text zu platzieren.

  • CSSOM-Baum erstellen. Ähnlich wie beim Erstellen von DOM erstellt der Browser auch Stilregeln in CSSOM. Der Browser durchläuft die in CSS festgelegten Regeln und erstellt einen Knotenbaum mit Eltern-Kind-, Geschwister- usw. Beziehungen basierend auf dem CSS-Selektor.

  • Erstellen Sie den Render-Baum. Dieser Schritt ordnet DOM und CSSOM zu und bestimmt, welche CSS-Regeln auf jedes DOM-Element angewendet werden sollen. Ordnen Sie alle relevanten Stile jedem sichtbaren Knoten im DOM-Baum zu und bestimmen Sie den berechneten Stil jedes Knotens basierend auf der CSS-Kaskade. Unsichtbare Knoten (Kopf, Knoten mit Attributen einschließlich display:none) werden nicht im Render-Baum generiert.

  • Layout/Reflow. Das erste Mal, dass der Browser die Position und Größe des Knotens bestimmt, wird als Layout bezeichnet. Wenn sich die Position und Größe des Knotens anschließend ändert, löst dieser Schritt eine Layoutanpassung aus, die als „Reflow“ bezeichnet wird.

    Malen/Neulackieren
  • . Zeichnet jeden sichtbaren Teil eines Elements auf den Bildschirm, einschließlich Text, Farbe, Ränder, Schatten und ersetzte Elemente wie Schaltflächen und Bilder. Wenn sich
  • Text, Farbe, Rahmen, Schatten

    und andere Elemente ändern, wird Repaint ausgelöst. Um sicherzustellen, dass das erneute Zeichnen schneller erfolgt als das erste Zeichnen, wird die Zeichnung auf dem Bildschirm häufig in mehrere Ebenen unterteilt. Durch das Heraufstufen von Inhalten auf die GPU-Ebene (was durch Transformation, Filter, Willensänderung und Deckkraft ausgelöst werden kann) kann die Leistung beim Zeichnen und Neuzeichnen verbessert werden.

    Compositing
  • . Dieser Schritt führt die Ebenen im Zeichenprozess zusammen und stellt sicher, dass sie in der richtigen Reihenfolge gezeichnet werden, um den richtigen Inhalt auf dem Bildschirm anzuzeigen.
  • Warum virtuelles DOM benötigt wird

  • Das Obige ist ein DOM-Rendering-Prozess. Wenn der Dom aktualisiert wird, muss der Dom erneut gerendert werden. Wenn die folgende Situation vorliegt, muss das DOM tatsächlich betrieben werden 100 W-mal, löst 1 Million Mal Reflow aus. Bei jeder DOM-Aktualisierung erfolgt die Aktualisierung des echten DOM ohne Unterschiede. Es verursachte also eine Menge Leistungsverlust. Wenn es in der Schleife komplexe Vorgänge gibt, die häufig Reflow und Neuzeichnen auslösen, kann dies leicht die Leistung beeinträchtigen und Verzögerungen verursachen. Darüber hinaus ist hier zu beachten, dass virtuelles DOM nicht bedeutet, dass es schneller ist als DOM. Die Leistung von virtuellem DOM hängt positiv von der Vorlagengröße ab. Der Vergleichsprozess des virtuellen DOM unterscheidet nicht die Größe der Daten. Wenn sich nur wenige dynamische Knoten innerhalb der Komponente befinden, durchläuft das virtuelle DOM dennoch das gesamte VDOM, was im Vergleich zum direkten Rendern eine zusätzliche Operationsebene darstellt.
<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>
Nach dem Login kopieren

Zum Beispiel im obigen Beispiel virtuelles DOM. Obwohl es nur einen dynamischen Knoten gibt, muss das virtuelle DOM dennoch die Klasse, den Text, die Beschriftung und andere Informationen der gesamten Diff-Liste durchlaufen, und schließlich ist noch ein DOM-Rendering erforderlich. Wenn es sich nur um eine DOM-Operation handelt, müssen Sie nur ein bestimmtes DOM bedienen und es dann rendern. Der Kernwert des virtuellen DOM besteht darin, dass es das reale DOM durch js beschreiben kann. Durch deklarative Sprachoperationen bietet es Entwicklern in den meisten Szenarien eine bequemere und schnellere Entwicklungserfahrung und erfordert keine manuelle Optimierung Die Leistungsgrenze ist gewährleistet und das Preis-/Leistungsverhältnis ist höher.

Virtuelles DOM

Virtuelles DOM ist im Wesentlichen ein js-Objekt, das die reale DOM-Struktur durch Objekte darstellt. Tags dienen zur Beschreibung von Tags, Requisiten zur Beschreibung von Attributen und untergeordnete Elemente werden zur Darstellung verschachtelter hierarchischer Beziehungen verwendet.

<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>
Nach dem Login kopieren

Bei der Aktualisierung des virtuellen DOM wird das DOM nicht sofort ausgeführt, sondern es wird der Diff-Algorithmus verwendet, um die Knoten zu finden, die aktualisiert werden müssen, und den aktualisierten Inhalt anschließend als js-Objekt zu speichern Das Update ist abgeschlossen und wird auf dem echten Dom gemountet. Realisieren Sie das Update des echten Doms. Durch virtuelles DOM werden drei Probleme beim Betrieb von realem DOM gelöst.

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

  • 频繁回流与重绘

  • 开发体验

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

优点

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

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

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

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

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

不足

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

虚拟DOM实现原理

主要分三部分

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

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

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

Diff算法

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

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

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

  • 创建新节点

  • 删除废节点

  • 更新已有节点

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

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)
}
Nach dem Login kopieren

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

创建新节点

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

删除旧节点

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

更新节点

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

  • 判断新节点与旧节点是否完全一致,一样的话就不需要更新
  // 判断vnode与oldVnode是否完全一样
  if (oldVnode === vnode) {
    return;
  }
Nach dem Login kopieren
  • 判断新节点与旧节点是否是静态节点,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;
  }
Nach dem Login kopieren
  • 判断新节点是否有文本(通过text属性判断),如果有文本那么需要比较同层级旧节点,如果旧节点文本不同于新节点文本,那么采用新的文本内容。如果新节点没有文本,那么后面需要对子节点的相关情况进行判断
//判断新节点是否有文本
if (isUndef(vnode.text)) {
  //如果没有文本,处理子节点的相关代码
  ....
} else if (oldVnode.text !== vnode.text) {
  //新节点文本替换旧节点文本
  nodeOps.setTextContent(elm, vnode.text)
}
Nach dem Login kopieren
  • 判断新节点与旧节点的子节点相关状况。这里又能分为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)
}
Nach dem Login kopieren

只有新节点有子节点

只有新节点有子节点,那么就代表着这是新增的内容,那么就是新增一个子节点到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
          }
      }
  }
}
Nach dem Login kopieren

只有旧节点有子节点

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

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

都没有子节点

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

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

整体的逻辑代码如下

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)
    }
}
Nach dem Login kopieren

配上流程图会更清晰点

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

子节点的比较更新updateChildren

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

const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];
Nach dem Login kopieren

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

比较过程

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

向中间靠拢

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

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

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

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

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

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

子节点比较过程

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

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

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

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

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

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

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

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]
Nach dem Login kopieren
  • 新前与旧后的比较,如果他们相同,就进行更新,然后将旧后移动到所有未处理旧节点数组最前面,是旧后与新前位置保持一致,,然后新向后,旧向前,继续向中间靠拢。继续比较剩余的节点。如果不同,就使用传统的循环遍历查找。

Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

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

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

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

然后拿新节点的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
  }
}
Nach dem Login kopieren

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

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

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

1Vertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue

//如果新节点无法在旧节点中找到自己的位置下标,说明是新元素,执行新增操作
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]
Nach dem Login kopieren

当完成对旧节点的遍历,但是新节点还没完成遍历,那就说明后续的都是新增节点,执行新增操作,如果完成对新节点遍历,旧节点还没完成遍历,那么说明旧节点出现冗余节点,执行删除操作。

//完成对旧节点的遍历,但是新节点还没完成遍历,
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)
}
Nach dem Login kopieren

子节点比较总结

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

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)
    }
}
Nach dem Login kopieren

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

Das obige ist der detaillierte Inhalt vonVertiefendes Verständnis des virtuellen DOM- und Diff-Algorithmus in Vue. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:juejin.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!