В источнике Vue (девять) — VirtualDOM и путь

внешний интерфейс алгоритм исходный код JavaScript Vue.js

Какова сегодня связь между VirtualDom и путем?

VNode (VirtualDom)

Прежде чем произойдет двусторонняя привязка, нам нужно напрямую манипулировать узлами DOM в каждом методе события триггера, чтобы достичь цели изменения соответствующего представления. Но когда приложение большое, его становится сложно поддерживать, а перекомпоновка влияет на производительность.

Следовательно, кто-то предложил, чтобы мы могли абстрагировать настоящее дерево DOM в абстрактное дерево, состоящее из объектов JavaScript, а затем преобразовать абстрактное дерево в настоящий DOM и перерисовать его на странице после изменения данных абстрактного дерева? Так появился виртуальный DOM, который представляет собой слой абстракции реального DOM и использует атрибуты для описания различных характеристик реального DOM. Когда он изменяется, вид изменяется.

Можно предположить, что если на странице с innerHTML модифицировать всю DOM-структуру самым простым и грубым способом, то перерисовывать таким образом весь слой представления достаточно затратно по производительности. время? Поэтому Vue.js абстрагирует DOM в виртуальное дерево DOM с объектами JavaScript в качестве узлов, имитирует реальный DOM с узлами VNode и может создавать узлы, удалять узлы и изменять узлы в этом абстрактном дереве. , и только разница должна быть изменена после манипулирования объектом JavaScript.По сравнению с грубой модификацией innerHTML всего блока, производительность значительно повышается. После модификации некоторые минимальные единицы, которые необходимо изменить, получаются с помощью алгоритма diff, а затем обновляются представления этих небольших единиц. Это уменьшает количество ненужных манипуляций с DOM и значительно повышает производительность.

Vue использует такой абстрактный узел VNode, который является уровнем абстракции реального DOM и не зависит от конкретной платформы — это может быть платформа браузера или weex, и даже платформа узла может абстрагировать такое дерево. Дерево DOM выполняет такие операции, как создание, удаление и модификация, что также обеспечивает возможность изоморфизма внешнего и внутреннего интерфейса.

Детали конкретного VNode можно увидетьАнализ исходного кода Vue (7) — генерация рендера в VNode.

Как изменить представление?

В предыдущей статье было показано, что Vue изменяет представление через привязку данных.Когда определенные данные изменяются, метод set заставит Dep в вызове закрытия уведомить об уведомлении всех подписчиков Watcher, а Watcher выполнит vm._update( vm._render() , увлажнение).

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    /*如果已经该组件已经挂载过了则代表进入这个步骤是个更新的过程,触发beforeUpdate钩子*/
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    /*基于后端渲染Vue.prototype.__patch__被用来作为一个入口*/
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    /*更新新的实例对象的__vue__*/
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

Первый параметр метода обновления — это объект VNode, который внутренне __исправит_ объект VNode старым объектом VNode.

Так что же такое путь?

path

Патч сравнивает новый и старый узлы VNode, а затем изменяет представление в наименьшей единице в соответствии с результатом сравнения двух вместо перерисовки всего представления в соответствии с новым VNode. Ядром исправления является алгоритм сравнения, который может эффективно сравнивать изменения с виртуальной моделью модели DOM и получать изменения для изменения представления.

Итак, как работают патчи?

Прежде всего, давайте поговорим об основном алгоритме diff патча.Алгоритм diff достигаетсяузел дерева на том же уровнеСравнение выполняется вместо послойного поиска и обхода дерева, поэтому временная сложность составляет всего O(n), что является достаточно эффективным алгоритмом.

Эти две картинки представляют процесс установки патчей между старым VNode и новым VNode.VNode на том же уровнеИзменяется сравнение между ними (квадраты одного цвета на втором рисунке представляют узлы VNode, которые сравниваются друг с другом), а затем измененное представление модифицируется, что очень эффективно.

Из предыдущего введения мы знаем, что нам нужно преобразовать VNode в настоящий узел DOMe, который необходимо реализовать с помощью функции patch:

vm.$el = vm.__patch__(prevVnode, vnode)

И __patch__ определяется в platform/web/runtime/index.js:

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

В основном это делается для того, чтобы определить, находится ли текущая среда в среде браузера, то есть есть ли объект Window. Это также для кросс-платформенной обработки.Если это происходит в среде рендеринга сервера, то патч не работает. Затем давайте посмотрим на исходный код пути (src/core/vdom/patch.js).

/*createPatchFunction的返回值,一个patch函数*/
return function patch (oldVnode, vnode, hydrating, removeOnly) {
/*vnode不存在则直接调用销毁钩子*/
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      /*oldVnode未定义的时候,其实也就是root节点,创建一个新的节点*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
    /*标记旧的VNode是否有nodeType*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        /*是同一个节点的时候直接修改现有的节点*/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          /*当旧的VNode是服务端渲染的元素,hydrating记为true*/
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
          /*需要合并到真实DOM上*/
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
             /*调用insert钩子*/
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
           /*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        /*取代现有元素*/
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        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)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
        /*组件根节点被替换,遍历更新父节点element*/
          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) {
            /*调用create回调*/
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
        /*移除老节点*/
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
        /*调用destroy钩子*/
          invokeDestroyHook(oldVnode)
        }
      }
    }
    /*调用insert钩子*/
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

Здесь функция createPatchFunction используется для создания и возврата функции исправления. путь получает 6 параметров:

1.oldVnode: старый виртуальный узел или старый узел реального дома

2.vnode: новый виртуальный узел

3.увлажнение: слиться ли с настоящим домом

4.removeOnly: специальный флаг для компонентов

5.parentElm: родительский узел

6.refElm: новый узел будет вставлен перед refElm.

Конкретный анализ см. в комментариях к коду. Помимо вызова хуков жизненного цикла и уничтожения узлов, мы обнаружили, что ключ к коду лежит в одних и тех же методах Vnode, createElm и patchVnode.

sameVnode

Давайте взглянем на реализацию sameVnode.

/*
  判断两个VNode节点是否是同一个节点,需要满足以下条件
  key相同
  tag(当前节点的标签名)相同
  isComment(是否为注释节点)相同
  是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义
  当标签是<input>的时候,type必须相同
*/
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
/*
  判断当标签是<input>的时候,type是否相同
  某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型
*/
function sameInputType (a, b) {
  if (a.tag !== 'input') 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
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}

createElm

function createElm (vnode,insertedVnodeQueue,parentElm, refElm,nested,ownerArray,index) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    // 用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。
  // 在重新激活组件中使用 insert 方法操作 DOM
    vnode.isRootInsert = !nested // for transition enter check
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
    // 错误检测,主要用于判断是否正确注册了component,这个错误还是比较常见
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      // nodeOps 封装的操作dom的合集
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)
       // weex处理
      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
      // 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法.
      // 如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(...) 在真实 DOM 中插入文本内容。
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // insert 用于将元素插入真实 DOM 中
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } else if (isTrue(vnode.isComment)) {// 注释
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } else { // 文本
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
  }

Из приведенных выше комментариев мы можем узнать, что конечной целью метода createElm является создание реального объекта DOM.

patchVnode

Давайте сначала посмотрим на код patchVnode.

/*patch VNode节点*/
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
/*两个VNode节点相同则直接返回*/
    if (oldVnode === vnode) {
      return
    }

    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
    }

    // 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.
    /*
      如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),
      并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),
      那么只需要替换elm以及componentInstance即可。
    */
    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 = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/
      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)
    }
     /*如果这个VNode节点没有text文本时*/
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
      /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
      /*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
      /*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
       /*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
     /*当新老节点text不一样时,直接替换这段文本*/
      nodeOps.setTextContent(elm, vnode.text)
    }
    /*调用postpatch钩子*/
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

Правила для patchVnode следующие:

1. Если старый и новый виртуальные узлы являются статическими, а их ключи одинаковыми (представляющими один и тот же узел), а новый виртуальный узел клонируется или помечается один раз (отмеченный атрибут v-once, визуализируется только один раз), то необходимо только заменить elm и componentInstance могут быть.

2. Если и у старого, и у нового узла есть дочерние дочерние узлы, операция diff выполняется над дочерними узлами и вызывается updateChildren, который также является ядром diff.

3. Если у старого узла нет дочерних узлов, а у нового узла есть дочерние узлы, сначала очистите текстовое содержимое DOM старого узла, а затем добавьте дочерние узлы к текущему узлу DOM.

4. Когда у нового узла нет дочерних узлов, а у старого узла есть дочерние узлы, удалите все дочерние узлы узла DOM.

5. Когда старый и новый узлы не имеют дочерних узлов, это просто замена текста.

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)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
      /*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        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)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        /*
          生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)
          比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  
          结果生成{key0: 0, key1: 1, key2: 2}
        */
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        /*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
         /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
        /*获取同key的老节点*/
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
          /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
             /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
            oldCh[idxInOld] = undefined
            /*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
    /*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
    /*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }

Давайте многократно нарисуем диаграмму о общем процессе:

Может быть вы видите это или немного неясно в облаках, не важно, давайте переварим это по пунктам:

Определите исходные переменные:

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] // 新列表终点值

Во-первых, слева и справа от нового и старого узлов VNode есть переменный маркер, и эти переменные будут перемещаться ближе к середине в процессе обхода. Цикл завершается, когда oldStartIdx > oldEndIdx или newStartIdx > newEndIdx.

Соответствие между индексами и узлами VNode: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode

При обходе, если ключ существует и тот же Vnode удовлетворяется, узел DOM будет использоваться повторно, в противном случае будет создан новый узел DOM.

Во-первых, всего существует 2*2=4 метода сравнения для парного сравнения oldStartVnode, oldEndVnode и newStartVnode, newEndVnode.

Когда начало или конец старого и нового узлов VNode удовлетворяют одному и тому же Vnode, т. е. sameVnode(oldStartVnode, newStartVnode) или sameVnode(oldEndVnode, newEndVnode), вы можете напрямую patchVnode на узле VNode.

Если oldStartVnode и newEndVnode удовлетворяют sameVnode, то есть sameVnode(oldStartVnode, newEndVnode).

В настоящее время это означает, что oldStartVnode работает после oldEndVnode.Когда patchVnode выполняется, реальный узел DOM должен быть перемещен за oldEndVnode.

Если oldEndVnode и newStartVnode удовлетворяют sameVnode, то есть sameVnode(oldEndVnode, newStartVnode).

Это показывает, что oldEndVnode располагался впереди oldStartVnode, а реальный узел DOM перемещался впереди oldStartVnode во время выполнения patchVnode.

Если ни одно из вышеперечисленных условий не выполняется, через createKeyToOldIdx будет получен oldKeyToIdx, в котором ключ — это старый VNode, а значение — хэш-таблица, соответствующая индексной последовательности. Из этой хэш-таблицы вы можете узнать, существует ли старый узел VNode с тем же ключом, что и newStartVnode.Если тот же самый Vnode одновременно удовлетворяется, patchVnode переместит реальный DOM (elmToMove) на передний план реального DOM, соответствующего на старыйStartVnode.

Конечно, также возможно, что newStartVnode не может найти непротиворечивый ключ в старом узле VNode, или даже если ключ тот же самый, но не тот же самый Vnode, тогда createElm будет вызван для создания нового узла DOM.

На этом цикл закончился, поэтому нам все еще нужно иметь дело с избытком или недостатком реальных узлов DOM.

1. Когда oldStartIdx > oldEndIdx в конце, старый узел VNode был пройден, а новый узел еще не пройден. Это показывает, что новые узлы VNode на самом деле больше, чем старые узлы VNode, то есть больше, чем реальный DOM.Необходимо вставить оставшиеся (то есть вновь добавленные) узлы VNode в реальные узлы DOM.В это время , вызовите addVnodes (пакетный вызов интерфейса createElm для добавления этих узлов в настоящий DOM).

2. Точно так же, когда newStartIdx > newEndIdx, новый узел VNode был пройден, но старый узел все еще остался, что указывает на то, что реальный узел DOM является избыточным и должен быть удален из документа.В это время вызовите removeVnodes, чтобы удалить эти избыточные узлы. настоящие узлы DOM.

Суммировать

На этом основные функции патча в основном закончены.Мы обнаружили, что в этой статье появилось большое количество ключевых полей. После вышеупомянутого исследования мы уже знаем, что ядро ​​алгоритма сравнения Vue основано на двух простых предположениях:

1. Два одинаковых компонента создают похожие структуры DOM, а разные компоненты создают разные структуры DOM.

2. Группу узлов одного уровня можно отличить по уникальным идентификаторам.Исходя из двух вышеприведенных предположений, сложность алгоритма Diff виртуального DOM снижается с O(n^3) до O(n). данные меняются, алгоритм Diff будет сравнивать узлы только на одном уровне:

Короче говоря, функция ключа в основном заключается в эффективном обновлении виртуального DOM. Кроме того, ключевой атрибут также используется при переключении перехода элементов с одинаковым именем тега в vue. Цель также состоит в том, чтобы позволить vue различать их, иначе vue только заменит свои внутренние атрибуты, не вызывая эффекта перехода.

Если вы заинтересованы в diff, я рекомендую вам прочитать эту статьюУглубленный принцип сравнения виртуальных DOM Vue2.x

благодарныйРанмо учитель,muwooпредоставленный материал.

Если вам это нравится, вы можете дать мне звезду,github