La 13a parte del parche de aprendizaje de código fuente VUE (actualización dom)

1. Descripción general

En el capítulo anterior, explicamos el principio del algoritmo diff. En este capítulo, veamos cómo vue implementa el proceso de parche a través de este algoritmo.

Recuerde que cuando hablamos de vm._update en el sexto capítulo, fue responsable de convertir Vnode en dom real, incluidos dos procesos de ramificación, la primera representación de dom y la posterior actualización de dom.

 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    ...
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    ...
  }

En el sexto capítulo, hemos aprendido el proceso de renderizado dom primero. En este capítulo continuamos con el proceso de la segunda rama.

Segundo parche

Utilizamos el siguiente ejemplo de actualización de dom, combinado con el código fuente para analizar.

 <div id="app">
    <button v-on:click="changeMsg">设置</button>
  <ul>
    <li v-for="item in items" :id="item.id">
    </li>
  </ul>
</div>


var vm = new Vue({
    el:"#app",
    data:{
      msg:'this is msg',
      items:[
      {id:1},
      {id:2},
      {id:3}
      ],
   methods:{
    changeMsg(){
      this.items.push({id:4});
    }
  }
})

Este ejemplo es relativamente simple, después de que se completa la representación dom como sigue:

Cuando se hace clic en el botón "Configuración", se agrega una columna adicional a la matriz. A partir del conocimiento previo, se puede ver que el cambio del valor del atributo del elemento desencadenará la actualización de su objeto observador de suscripción relacionado. Debido a que este atributo se usa en la plantilla, activará la actualización del render wacher, ejecutará vm._render () y producirá un nuevo vnode. vm. $ el = vm .__ patch __ (prevVnode, vnode), a través de la comparación de vnode nuevo y antiguo, para actualizar el dom real. La representación final es la siguiente:

Todo el proceso es que se agrega un nodo li, y los otros permanecen sin cambios. Esperamos que en la actualización DOM real, solo se agregue un nuevo nodo li en lugar de una actualización global, de modo que la eficiencia sea la más alta.

El método vm .__ patch __ (prevVnode, vnode) se define en última instancia en src / core / vdom / patch.js.

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    ....
     //1、oldVnode为空(首次渲染,或者组件),直接创建新的root element
    if (isUndef(oldVnode)) {//
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      //2、oldVnode存在,进入patchVnode过程
      const isRealElement = isDef(oldVnode.nodeType)//oldVnode是id为app的dom节点
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        ......
        //3、创建节点
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
       ...
  }

En el proceso patchVnode, miramos directamente el segundo paso, primero determinamos isRealElement y sameVnode. isRealElement indica si es un nodo dom real y luego mira el método sameVnode.

function sameVnode (a, b) {
  return (
    a.key === b.key && (//key相同,undefined也算相同
      (
        a.tag === b.tag &&//tag相同
        a.isComment === b.isComment &&//是否是comment
        isDef(a.data) === isDef(b.data) &&//data,属性已定义
        sameInputType(a, b)//input相同
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

sameVnode es en realidad la comparabilidad de los dos nodos descritos, debe satisfacerse (el último o la condición se ignora temporalmente),

(1) La clave es la misma e indefinida también es la misma.

(2) La misma etiqueta, como div, ul

(3) El atributo de datos, si hay definiciones de atributo para ser coherente. Por ejemplo, el atributo id en el nodo li. (Nota: el atributo de datos no necesita ser el mismo)

(4) Tipo de entrada. El tipo de tipo es el mismo, tanto si la definición de datos debe ser consistente.

Es decir, si se cumplen las condiciones anteriores, es elegible realizar parches entre nodos y continuar la comparación de nodos secundarios; de lo contrario, vaya directamente a la rama else para crear e insertar un nuevo nodo.

     Aquí hablamos de la clave: durante el proceso de desarrollo, cuando usamos v-for, input y otras etiquetas, necesitamos agregar una clave para indicar un identificador único, de lo contrario encontraremos "cadenas" entre elementos y no obtendremos Nuestro efecto esperado. En este ejemplo, no agregamos la clave, la clave no está definida, por lo que para juzgar la comparabilidad, solo se adopta la última condición. Para el nodo li con id = "1", se usa "reutilización en el lugar". A veces, no queremos hacer esto, solo necesitamos configurar las claves de estos dos elementos para que sean diferentes, luego la comparabilidad de estos dos elementos no es suficiente, y el nodo del elemento será recreado.

El nodo raíz div en este ejemplo satisface esta condición, así que ingrese el proceso patchVnode.

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
   ...

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    //1、对于static节点树,无需比较,直接节点复用
    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)
    }
    
    //获取oldVnode,Vnode的子节点,进行比较
    const oldCh = oldVnode.children
    const ch = vnode.children
    //2、更新当前节点,执行update相关方法
    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)
    }
    //3、比较子节点
    if (isUndef(vnode.text)) {//3.1 非text节点叶节点
      if (isDef(oldCh) && isDef(ch)) {//新,旧子节点存在
        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(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {//旧节点的为text节点,则设置为空
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {//3.2 text叶节点,但是text不同,直接更新
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

El proceso central de patchVnode se divide en tres partes.

1. Determine si es estático. En la sección de compilación, presentamos lo que es un nodo estático. Para mejorar la eficiencia del parche, para los nodos estáticos, reutilice los elementos directamente.

2. Actualice el nodo actual y ejecute el método de actualización relevante. Estos métodos son los siguientes, principalmente para actualizar el contenido definido en el atributo de datos. (sameVnode no requiere el mismo motivo para el atributo de datos)

updateAttrs (oldVnode, vnode)
updateClass (oldVnode, vnode)
updateDOMListeners (oldVnode, vnode)
updateDOMProps (oldVnode, vnode)
updateStyle (oldVnode, vnode)
update (oldVnode, vnode)
updateDirectives (oldVnode, vnode, vnode, vnode)

3. Comparar nodos secundarios.

(1) Para nodos hoja sin texto, continúe comparando nodos secundarios. Si existen los nodos secundarios nuevos y antiguos, se llama a updateChildren para continuar la comparación; si el nuevo nodo secundario existe, el nodo secundario anterior no existe, significa que el nodo secundario es nuevo y se crea llamando a addVnodes; si el nuevo nodo secundario no existe, existe el nodo secundario anterior, lo que significa que el nodo secundario existe Es redundante, llame a removeVnodes para eliminar el nodo.

(2) Para el nodo hoja de texto, si el contenido del texto es diferente, se actualiza directamente.

En este ejemplo, los nodos secundarios del elemento ul, los nodos nuevos y antiguos tienen todos los nodos secundarios li. Entonces ingrese el método updateChildren.

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 !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {// os没有定义,os索引向后移动一位
        oldStartVnode = oldCh[++oldStartIdx] 
      } else if (isUndef(oldEndVnode)) {//oe没有定义,oe索引向前移动一位
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {//os==ns,保持节点位置不变,继续比较其子节点,os,ns索引向后移动一位
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {//oe==ne,保持节点位置不变,继续比较其子节点,oe,ne索引向后前移动一位。
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // os==ne,将os节点移动到oe后面,继续比较其子节点,os索引向后移动一位,ne索引向前移动一位
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // oe==ns,将oe移动到os节点前,继续比较其子节点,oe索引向后移动一位,ns向前移动一位。
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {//两组都不相同的情况下
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        //在oldstartIdx与oldEndIdx间,查找与newStartVnode相同(key相同,或者节点元素相同)节点索引位置
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          //没有相同节点,则将newStartVnode插入到oldStartVnode前
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          //key值和节点都都相同
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            //移动到oldStartVnode前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            //相同的key,但节点元素不同,创建一个新的newStartVnode节点,插入到oldStartVnode前。
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        //newStartVnode索引加1
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {//旧节点先遍历完,新增剩余的新节点,并插入到ne+1前位置
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {//新节店先遍历完,删除剩余的老节点
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

El método updateChildren es la realización del método diff, y puede leer el código de acuerdo con el diagrama del capítulo anterior.

Inicialice el comienzo de los nodos nuevos y antiguos y finalice el índice. Representado por os (oldStartIdx), oe (oldEndIdx), ns (newStartIdx), ne (newEndIdx) respectivamente.

(1) el sistema operativo no está definido y el índice del sistema operativo retrocede un bit.

(2) oe no está definido, y el índice oe avanza un bit.

(3) os == ns, mantenga la posición del nodo sin cambios, continúe comparando sus nodos hijos, el índice os, ns se mueve un poco hacia atrás.

(4) oe == ne, mantenga la posición del nodo sin cambios, continúe comparando sus nodos hijos, el índice de oe, ne se mueve un poco hacia atrás y hacia adelante.

(5) os == ne, continúe comparando sus nodos hijos, mueva el nodo os detrás de oe, el índice os retrocede un bit y el índice ne avanza un bit.

(6) oe == ns, continúe comparando sus nodos hijos, mueva oe al nodo os, oe index se mueve un bit hacia atrás y ns se mueve un bit hacia adelante.

(7) Cuando los dos grupos no son iguales, entre os y oe, encuentre el nodo que es el mismo que newStartVnode (la misma clave o el mismo elemento de nodo), si el nodo no existe, cree un nuevo nodo e insértelo antes de os ; Si el nodo existe, mueva el nodo a os. El índice ns se mueve hacia atrás un bit.

(8) Primero se atraviesa el antiguo nodo, se agrega el nuevo nodo restante y se inserta en la posición antes de ne + 1.

(9) Primero se atraviesa el nuevo nodo y se eliminan los nodos antiguos restantes.

En este ejemplo, el nodo li, los primeros tres de los nodos nuevos y antiguos son iguales, y se agrega el último, por lo que el octavo caso de ejecución cumple con nuestras expectativas.

Entre ellos, en el caso de 3,4,5,6, llame a patchVnode para actualizar el nodo y continúe comparando los nodos secundarios hasta que llegue al nodo hoja.

3. Resumen

Todo el proceso consiste en mapear la operación dom real a través de la comparación entre el nuevo y antiguo Vnode y el algoritmo diff.

33 artículos originales publicados · Me gustaron 95 · Visitantes 30,000+

Supongo que te gusta

Origin blog.csdn.net/tcy83/article/details/94329331
Recomendado
Clasificación