Vue source code learning-dom virtual y algoritmo diff

prefacio

Esta es una serie de artículos sobre el aprendizaje del código fuente. Si está interesado, puede continuar leyendo otros artículos
Aprendizaje del código fuente de Vue: ¿qué hace la nueva inicialización de Vue?
Aprendizaje de código fuente de Vue: principio de respuesta a datos
Aprendizaje de código fuente de Vue: cola de actualización asíncrona y principio nextTick

Debido a que el algoritmo Diff calcula la diferencia del DOM virtual, es mejor colocar primero un poco de DOM virtual, comprender su estructura y luego observar el principio del algoritmo Diff.

Al renderizar el DOM real, no sobrescribe violentamente el DOM original, sino que compara los vnodes (nodos virtuales) antiguos y nuevos. Si no es el mismo nodo, elimine el anterior y reemplácelo por uno nuevo; si lo es el mismo nodo, simplemente reutilice los nodos antiguos y agregue los atributos de los nuevos nodos.

Primero, entienda el DOM virtual

DOM virtual es simplemente eso 用JS对象来模拟 DOM 结构.

Simule la estructura DOM con objetos JS

Ejemplo de simulación de estructura DOM con objetos JS:

<template>
    <div id="app" class="container">
        <h1>铁锤妹妹</h1>
    </div>
</template>

La plantilla anterior se convierte en un objeto JS de la siguiente manera.
Tal estructura DOM se llama 虚拟 DOM(Nodo Virtual), abreviado vnode.

{
    
    
  tag:'div',
  props:{
    
     id:'app', class:'container' },
  children: [
    {
    
     tag: 'h1', children:'铁锤妹妹' }
  ]
}

Su expresión es convertir cada etiqueta en un objeto, que puede tener tres atributos: tag, props, children.

  • etiqueta : requerida. Incluso las etiquetas pueden ser componentes o funciones.
  • accesorios : opcional. Son las propiedades y métodos en esta etiqueta.
  • niños : opcional. Es el contenido o los nodos secundarios de esta etiqueta. Si es un nodo de texto, es una cadena, y si tiene nodos secundarios, es una matriz. En otras palabras, si se considera que child es una cadena, debe ser un nodo de texto y este nodo no debe tener elementos secundarios.

Los beneficios de simular nodos DOM con objetos JS

Suponiendo que hay 1000 nodos que deben actualizarse en una sola operación, entonces el DOM virtual no operará el Dom de inmediato, sino que guardará el contenido diferencial de estas 1000 actualizaciones en uno local y luego colocará este objeto JS en el árbol JS对象DOM attach. en un momento, Finalmente, lleve a cabo la operación de seguimiento, así 避免了大量没必要的计算.

Por lo tanto, la ventaja de simular nodos DOM con objetos JS es: primero reflejar todas las actualizaciones de la página en 虚拟DOM, para que 先操作内存中的JS对象. Vale la pena señalar que 操作内存中的 JS对象 速度是相当快的. Luego espere hasta que se actualicen todos los nodos DOM, y luego JS对象asigne el último 真实DOMy entrégueselo al navegador para que lo dibuje.

Esto resuelve los problemas de renderizado DOM real lento y consumo de alto rendimiento .

¿Por qué usar DOM virtual?

Primero creemos un div vacío e imprimamos para ver todos los atributos y eventos que vienen con él.

    let div = document.createElement('div')
    let props = ''
    for (let key in div) {
    
    
      props += key + ' '
    }
    console.log(props)

Imprimir resultado:

inserte la descripción de la imagen aquí

Como se puede ver en la figura, el DOM nativo tiene muchos atributos y eventos, incluso crear un div vacío costará mucho. El punto de usar el DOM virtual para mejorar el rendimiento es que cuando cambia el DOM, el algoritmo de diferencias se compara con el DOM antes de que cambien los datos para calcular el DOM que debe cambiarse y luego solo operar en el DOM modificado en lugar de actualizar el vista completa.

La relación entre el Dom virtual y el algoritmo diff

De hecho, vdomes un gran concepto, pero diff算法una parte de vdom. El valor central de vdom radica en 最大程度上减少 真实DOM 的频繁更新.
vdom usa DOM para JS的方式simular, compara el DOM virtual nuevo y antiguo, solo actualiza la diferencia y luego opera el DOM real en lotes, lo que reduce las operaciones frecuentes en el DOM real y mejora el rendimiento. Entonces el proceso de comparación es diff算法. En otras palabras, ambos son 包含关系, como se muestra en la siguiente figura:
inserte la descripción de la imagen aquí

En segundo lugar, comprenda el algoritmo diff

Optimización del algoritmo diff

Si hay 1000 nodos, se necesita calcular 1000³ veces, es decir, 1 billón de veces, lo cual es inaceptable, por lo que al usar el algoritmo Diff en Vue, sigue la estrategia de algunas optimizaciones 深度优先para 同层比较calcular 最小变化.

1)只比较同一层级,不跨级比较

El proceso Diff solo compara el DOM enmarcado por el mismo color 同一层级, lo que simplifica el número de comparaciones.
inserte la descripción de la imagen aquí

2) Si los nombres de las etiquetas del mismo nivel son diferentes, elimine el DOM virtual anterior directamente y reconstruya sin continuar con la comparación en profundidad.比较tag标签名

inserte la descripción de la imagen aquí

3) Si son iguales, también son iguales, se considerará que sí , y no se continuará con la comparación en profundidad. Por ejemplo, cuando escribimos v-for, compararemos claves, y si no escribimos claves, se informará un error, esto se debe a que el algoritmo Diff necesita comparar claves.比较 key
标签名key相同节点

El papel de la llave

Se entenderá mejor a través de ejemplos gráficos:
por ejemplo, si hay una lista y se necesita insertar un elemento en el medio de la lista, ¿qué sucederá? primer vistazo a una imagen
inserte la descripción de la imagen aquí

Como se muestra en la figura, li1 y li2 no se volverán a renderizar, pero li3, li4 y li5 se volverán a renderizar.

Porque cuando la clave o el índice de la lista no se usa como clave, la relación posicional correspondiente a cada elemento es el índice índice.El resultado en la figura anterior conduce directamente a la inserción del elemento a todos los elementos siguientes, y se produce la relación posicional correspondiente. Cambie, por lo que todos realizarán operaciones de actualización, esto no es lo que queremos, lo que queremos es representar solo el elemento agregado li5, sin ningún cambio en los otros cuatro elementos, solo reutilícelo en su lugar, haga no volver a renderizar.

En el caso de usar una clave única, la relación posicional correspondiente a cada elemento es la clave, fíjate en el caso de usar un valor de clave único:

inserte la descripción de la imagen aquí

De esta forma, li3 y li4 en la figura no se volverán a representar, porque el contenido de los elementos no ha cambiado y la relación de posición correspondiente no ha cambiado.
Esta es la razón por la cual v-for debe escribir la clave y no se recomienda usar el índice de la matriz como clave en el desarrollo.

en conclusión:

  • La función de la clave es principalmente actualizar el DOM virtual de manera más eficiente, ya que puede encontrar el mismo nodo con mucha precisión y la operación diff puede ser más eficiente.
    Si el orden de los elementos de datos ha cambiado, Vue no moverá los elementos DOM para que coincidan con el orden de los elementos de datos, sino simplemente " 就地复用" cada elemento aquí.

¿Cuándo se ejecuta el algoritmo diff?

1. 首次渲染Cuando se abra la página, se llamará al parche una vez y se creará un nuevo vnode, y no se realizará una comparación más profunda.
2. Luego 组件中的数据发生变化se activará en el momento de , settery luego a través notify()de la notificación watcher, el correspondiente watchernotificará la actualización y ejecutará la función de actualización, que ejecutará renderla función para obtener el nuevo DOM virtual, y luego realizará la patchcomparación con el antiguo DOM virtual, y calcule el cambio mínimo, luego vaya a actualizar de acuerdo con este cambio mínimo 真实的DOM, es decir, vea la actualización.

3. Código fuente del algoritmo de diferencias en profundidad

función de parche

La función central utilizada para comparar los VNodes antiguos y nuevos y actualizar el DOM.

Cabe señalar que la función de parche reutilizará los elementos y nodos DOM existentes tanto como sea posible al actualizar el DOM, a fin de mejorar el rendimiento. Solo actualizará las partes que realmente han cambiado comparando las diferencias entre el VNode antiguo y el nuevo, sin recrear toda la estructura DOM.

El proceso principal es este:

  • Si no existe vnode y existe oldVnode, elimine oldVnode. (La ausencia de vnode significa que el componente se eliminó o ya no es necesario procesarlo. Para mantener la vista y los datos sincronizados, elimine oldVnode)

  • Si existe vnode, si oldVnode no existe, cree vnode.

  • Si ambos existen, sameVnode()compare si son el mismo nodo por .

    1) Si es el mismo nodo, utilice patchVnode()la función para realizar comparaciones posteriores 节点文本变化o 子节点变化.
    2) Si no es el mismo nodo, elimine el nodo y vuelva a crear un nuevo nodo para reemplazarlo
    (para los nodos de componentes, Vue reutilizará las instancias de componentes existentes tanto como sea posible en lugar de destruir y recrear componentes)

// src/core/vdom/patch.ts

// 两个判断函数
export function isUndef(v: any): v is undefined | null {
    
    
  return v === undefined || v === null
}

export function isDef<T>(v: T): v is NonNullable<T> {
    
    
  return v !== undefined && v !== null
}

 return function patch(oldVnode, vnode, hydrating, removeOnly) {
    
    
   // 当新的 VNode 不存在时,如果旧的 VNode 存在,则调用旧的 VNode 的销毁钩子函数,以确保在组件更新过程中正确地执行销毁逻辑。
   // 如果新的 VNode 不存在,通常表示组件 被移除 或者 不再需要渲染。
   // 如果旧的 VNode 仍然存在,它对应的 DOM 元素需要被删除,以保持视图与数据的同步。确保不留下无用的 DOM 节点,避免内存泄漏和不必要的性能开销。
    if (isUndef(vnode)) {
    
    
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue: any[] = []

  // 如果 oldVnode 不存在的话,新的 vnode 是肯定存在的,比如首次渲染的时候
    if (isUndef(oldVnode)) {
    
    
      isInitialPatch = true
       // 就创建新的 vnode
      createElm(vnode, insertedVnodeQueue)
    } else {
    
    
    // 剩下的都是新的 vnode 和 oldVnode 都存在的话
    
    // 旧的 VNode是不是元素节点
      const isRealElement = isDef(oldVnode.nodeType)
     // 如果旧的 VNode 是真实的 DOM 元素节点 && 与新的 VNode 是同一个节点
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
    
    
        // 如果是,就用 patchVnode 对现有的根节点进行更新操作,而不是重新创建整个组件树。
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
    
    
      // 如果不是同一元素节点的话
        if (isRealElement) {
    
    
          // const SSR_ATTR = 'data-server-rendered'
          // 如果是元素节点 并且有 'data-server-rendered' 这个属性
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    
    
          // 就是服务端渲染,删掉这个属性
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          // 就是服务端渲染的,删掉这个属性
          if (isTrue(hydrating)) {
    
    
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
    
    
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (__DEV__) {
    
    
              warn('一段很长的警告信息')
            }
          }
          // 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 拿到 oldVnode 的父节点
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

         // 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

       // 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
        if (isDef(vnode.parent)) {
    
    
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          // 递归更新父节点下的元素
          while (ancestor) {
    
    
           // 卸载老根节点下的全部组件
            for (let i = 0; i < cbs.destroy.length; ++i) {
    
    
              cbs.destroy[i](ancestor)
            }
            // 替换现有元素
            ancestor.elm = vnode.elm
            if (patchable) {
    
    
              for (let i = 0; i < cbs.create.length; ++i) {
    
    
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
    
    
                for (let i = 1; i < insert.fns.length; i++) {
    
    
                  insert.fns[i]()
                }
              }
            } else {
    
    
              registerRef(ancestor)
            }
            // 更新父节点
            ancestor = ancestor.parent
          }
        }

        // 如果旧节点还存在,就删掉旧节点
        if (isDef(parentElm)) {
    
    
          removeVnodes([oldVnode], 0, 0)
        // 否则直接卸载 oldVnode
        } else if (isDef(oldVnode.tag)) {
    
    
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 返回更新后的节点
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

misma función Vnode

Esta es la función utilizada para juzgar 新旧Vnodesi 同一节点.

function sameVnode(a, b) {
    
    
  return (
    a.key === b.key &&  // key 是不是一样
    a.asyncFactory === b.asyncFactory &&  // 是不是异步组件
    ((a.tag === b.tag &&  // 标签是不是一样
      a.isComment === b.isComment &&  // 是不是注释节点
      isDef(a.data) === isDef(b.data) &&  // 内容数据是不是一样
      sameInputType(a, b)) ||   // 判断 input 的 type 是不是一样
      (isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))   // 判断区分异步组件的占位符否存在
  )
}

función patchVnode

Esta es una función que se ejecutará cuando el nuevo vnode y el oldVnode sean el mismo nodo, principalmente para comparar los cambios del texto del nodo o los nodos secundarios.

El proceso principal es este:

  • Si las direcciones de referencia de oldVnode y vnode son las mismas, significa que el nodo no ha cambiado y regresa directamente.

  • Si existe isAsyncPlaceholder de oldVnode, omita la verificación del componente asíncrono y regrese directamente.

  • Si tanto oldVnode como vnode son nodos estáticos && tienen la misma clave && vnode es un nodo controlado por el comando clone node || v-once, copie oldVnode.elm y oldVnode.child en vnode y luego regrese.

  • Si vnode no es ni un nodo de texto ni un comentario

    1) Si tanto oldVnode como vnode tienen nodos secundarios y 子节点不一样, llame updateChildren()a la función para actualizar los nodos secundarios.
    2) Si solo vnodetiene nodos secundarios, llame addVnodes()a crear nodos secundarios.
    3) Si solo oldVnodetiene un nodo secundario, llame para removeVnodes()eliminar el nodo secundario.
    4) Si oldVnodees un nodo de texto, bórrelo.

  • Si vnode es un nodo de texto pero el contenido del texto es diferente al de oldVnode, actualice el texto

 function patchVnode(
    oldVnode,  // 旧的虚拟 DOM 节点
    vnode,   // 新的虚拟 DOM 节点
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly?: any
  ) {
    
    
  // 新老节点引用地址是一样的,return 返回
  // 比如 props 没有改变的时候,子组件就不做渲染,直接复用
    if (oldVnode === vnode) {
    
    
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
    
    
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = (vnode.elm = oldVnode.elm)
    
    // 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
    
    
      if (isDef(vnode.asyncFactory.resolved)) {
    
    
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
    
    
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // 当前节点是静态节点的时候,key 也一样,并且vnode 是克隆节点,或者有 v-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)) {
    
    
      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)) {
    
    
    // 如果新旧节点都有子节点
      if (isDef(oldCh) && isDef(ch)) {
    
    
      // 但是子节点不一样,就调用 updateChildren 函数,对比子节点
        if (oldCh !== ch)
          updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
    
    
      // 如果只有新节点有子节点的话,新增子节点
        
        // 如果 旧节点 是文本节点,表示它没有子节点,就清空
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 新增 子节点
        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, '')
      }
    // 新老节点都是文本节点,且文本不一样,就更新文本
    } 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)
    }
  }

función actualizar niños

Esta es una función nueva que vnodetiene oldVnodenodos secundarios y 子节点不一样compara nodos secundarios cuando .

这个函数 很关键,很关键!

Por ejemplo, ahora hay dos listas de nodos secundarios para comparar, el proceso principal de la comparación es el siguiente:

Bucle a través de dos listas, la condición de parada del bucle es: el puntero de inicio startIdx de una de las listas coincide con el puntero final endIdx .
El contenido del bucle es:

  • cabeza nueva vs cabeza vieja
  • Nueva cola contra vieja cola
  • Cola nueva vs cabeza vieja
  • Cabeza nueva vs cola vieja

Siempre que uno de los cuatro juicios anteriores sea igual, llame patchVnode()al cambio de texto del nodo de comparación o al cambio de nodo secundario y luego 移动对比的下标continúe con la siguiente ronda de comparación cíclica.

Si no se alcanza ninguna de las cuatro situaciones anteriores, se usará 循环para buscar, y la clave del nuevo nodo se usará para buscar en los elementos secundarios anteriores.

  • Si no lo encuentra, cree un nuevo nodo.

  • Si lo encuentra, compare si la etiqueta es el mismo nodo.

    1) Si es el mismo nodo, llame para pathVnode()realizar una comparación posterior, luego inserte este nodo 老的开始前面y mueva el nuevo subíndice de inicio para continuar con la siguiente ronda de comparación cíclica.
    2) Si no es el mismo nodo, cree un nuevo nodo.

  • Si el antiguo vnode se atravesó primero, agregue los nodos que el nuevo vnode no haya atravesado.

  • Si el nuevo nodo virtual se atravesó primero, significa que quedan nodos en el nodo antiguo y los nodos que el antiguo nodo virtual no atravesó se eliminarán.

¿Por qué hay operaciones cabeza a cola, cola a cabeza?

  • Head-to-tail y tail-to-head son una estrategia de optimización del algoritmo Diff, el propósito es minimizar el costo de volver a renderizar al volver a renderizar los nodos DOM existentes tanto como sea posible复用 .
  • La operación de cabeza a cola se refiere a comparar los pares de nodos en las posiciones inicial y final en las listas de nodos antigua y nueva, y luego mover la comparación paso a paso hacia el interior. La razón de esto es que, en muchos casos, el cambio de nodos ocurre principalmente al principio y al final de la lista, mientras que los nodos del medio son relativamente estables. A través de la comparación del primer y último nodo, se puede evitar el movimiento y la actualización de nodos innecesarios, y solo se deben insertar o eliminar los nodos recién agregados o eliminados.
  • Tail-to-head opera de manera similar a head-to-tail.
  function updateChildren(
    parentElm,
    oldCh,
    newCh,
    insertedVnodeQueue,
    removeOnly
  ) {
    
    
    let oldStartIdx = 0  // 老 vnode 遍历的开始下标
    let newStartIdx = 0  // 新 vnode 遍历的开始下标
    let oldEndIdx = oldCh.length - 1   // 老 vnode 遍历的结束下标
    let oldStartVnode = oldCh[0]   // 老 vnode 列表第一个子元素
    let oldEndVnode = oldCh[oldEndIdx]   // 老 vnode 列表最后一个子元素
    let newEndIdx = newCh.length - 1  // 新 vnode 遍历的结束下标
    let newStartVnode = newCh[0]  // 新 vnode 列表第一个子元素
    let newEndVnode = newCh[newEndIdx]   // 老 vnode 列表最后一个子元素
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    const canMove = !removeOnly

    if (__DEV__) {
    
    
      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
        )
        // 然后把指针后移一位,从前往后依次对比
        // 比如第一次对比两个列表[0],然后对比[1]...,后面同理
        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 {
    
    
        if (isUndef(oldKeyToIdx))
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
         // 拿到新开始的 key,在老的 children 里去找有没有某个节点有这个 key
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

       // 新的 children 里有,可是没有在老的 children 里找到对应的元素
        if (isUndef(idxInOld)) {
    
    
          // 创建新的元素
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        } else {
    
    
        // 在老的 children 里找到了对应的元素
          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]
      }
    }
    
   // 说明老的 vnode 先遍历完
    if (oldStartIdx > oldEndIdx) {
    
    
    // 就添加从 newStartIdx 到 newEndIdx 之间的节点
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(
        parentElm,
        refElm,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      )

    // 否则就说明新的 vnode 先遍历完
    } else if (newStartIdx > newEndIdx) {
    
    
    // 就删除老的 vnode 里没有遍历的结点
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

Resumir

1. El proceso de análisis del DOM virtual

Primero analice la estructura de árbol DOM que se insertará en el documento y utilícela js对象para expresarla, como un objeto de elemento, que incluye TagNamey propsestos Childrenatributos. Luego js对象树guarde esto y finalmente inserte el fragmento DOM en el documento.

Cuando el estado de la página cambia y es necesario ajustar la estructura del DOM de la página, primero se reconstruye un árbol de objetos de acuerdo con el estado modificado, y luego se ejecutan y para registrar las diferencias entre los 新的对象树dos 旧的对象树árboles 比较.

Finalmente, aplique las diferencias registradas a 真正的DOM树, para que la vista se actualice .

2. El principio del algoritmo diff

Al comparar el dominio virtual antiguo y el nuevo:

Primero, compare los propios nodos y sameVnode()juzgue si son el mismo nodo por .

  • Si no es el mismo nodo, elimine el nodo y cree un nuevo nodo para reemplazarlo.
  • Si es el mismo nodo, proceda patchVnode()a determinar cómo procesar los nodos secundarios de este nodo.

Primero juzgue la situación en la que una parte tiene nodos secundarios y la otra parte no tiene nodos secundarios.

1) Si los nuevos hijos tienen nodos secundarios, llame para addVnodes()crear nuevos nodos secundarios.
2) Si los nuevos hijos no tienen nodos secundarios, llame para removeVnodes()eliminar los nodos secundarios antiguos.

Si hay todos los nodos secundarios, pero los nodos secundarios son diferentes, proceda updateChildren()a determinar cómo operar en los nodos secundarios de estos nodos antiguos y nuevos (diff core).

Al hacer coincidir, se encuentran nodos secundarios idénticos y patchVnode()se llama a la función de forma recursiva para comparar y actualizar aún más esos nodos secundarios .

En diff, solo 同层se comparan los nodos secundarios y se abandona la comparación de nodos entre niveles, de modo que la complejidad temporal se reduce de O(n3) a O(n), es decir, solo cuando los elementos secundarios nuevos y antiguos son múltiples nodos secundarios Utilice el algoritmo Diff central para comparaciones del mismo nivel.

Puede consultar:
DOM virtual de celebridades de Internet en la entrevista, ¿cuánto sabe? Interpretación detallada del algoritmo diff,
explicación detallada del algoritmo virtual DOM y Diff, y la diferencia entre Vue2 y Vue3

Supongo que te gusta

Origin blog.csdn.net/weixin_45811256/article/details/131815014
Recomendado
Clasificación