Анализ алгоритма diff в vue распространенных проблем vue (4)

Предыстория: Прежде всего, алгоритм сравнения не является эксклюзивным для Vue.Пока используется инфраструктура виртуального дома, в основном будет использоваться алгоритм сравнения, так зачем использовать алгоритм сравнения и каковы преимущества алгоритма сравнения? Возьмем в качестве примера vue, проанализируем на уровне исходного кода и объясним необходимость алгоритма сравнения (метод mountComponent() в /src/core/instance/lifecycle.js), метода выполнения (/src/core/ vdom patchVnode() в /patch.js) и результирующая эффективность (метод updateChildren() в /src/core/vdom/patch.js).

1. Для анализа необходимости алгоритма diff обратитесь к исходному коду: метод mountComponent() в src/core/instance/lifecycle.js, исходный код выглядит следующим образом:

/*挂载组件*/
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    /*render函数不存在的时候创建一个空的VNode节点*/
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  /*触发beforeMount钩子*/
  callHook(vm, 'beforeMount')

  /*updateComponent作为Watcher对象的getter函数,用来依赖收集*/
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  /*这里对该vm注册一个Watcher实例,Watcher的getter为updateComponent函数,用于触发所有渲染所需要用到的数据的getter,进行依赖收集,该Watcher实例会存在所有渲染所需数据的闭包Dep中*/
  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    /*标志位,代表该组件已经挂载*/
    vm._isMounted = true
    /*调用mounted钩子*/
    callHook(vm, 'mounted')
  }
  return vm
}

Из исходного кода мы можем сделать вывод, что mountComponent() вызывается пользователем, когда $mount(), поэтому компонент вызовет $mount() один раз, поэтому также будет создан соответствующий наблюдатель, но может быть несколько данных в Компонент Для использования ключа в настоящее время существует только один наблюдатель.Если вы хотите узнать конкретный измененный ключ, вам нужно использовать алгоритм сравнения, чтобы сравнить старый и новый виртуальные дома, чтобы узнать конкретное изменение.

2. Метод выполнения алгоритма diff см. в исходном коде: метод patchVnode() в /src/core/vdom/patch.js, исходный код выглядит следующим образом:

  /*patch VNode节点*/
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    /*两个VNode节点相同则直接返回*/
    if (oldVnode === vnode) {
      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.elm = oldVnode.elm
      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 elm = vnode.elm = oldVnode.elm
    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 () — это место, где начинается diff, и основное внимание уделяется сравнению старых и новых виртуальных узлов.Из кода мы можем видеть, что стратегия сравнения: сначала в глубину, сравнение одного уровня, и ключевая производительность по-прежнему находится в методе 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, elmToMove, 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

    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] : null
        if (isUndef(idxInOld)) { // New element
          /*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          /*获取同key的老节点*/
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            /*如果elmToMove不存在说明之前已经有新节点放入过这个key的Dom中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }
          if (sameVnode(elmToMove, newStartVnode)) {
            /*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            /*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/
            oldCh[idxInOld] = undefined
            /*当有标识位canMove实可以直接插入oldStartVnode对应的真实Dom节点前面*/
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            /*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            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)
    }
  }

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

Подводя итог, можно сделать следующие выводы:

1. Алгоритм diff является продуктом виртуального дома: при сравнении старого и нового виртуального дома (т.е. diff) изменения обновляются в реальном доме, а временная сложность уменьшается с O(n^3) до O( n посредством эффективного сравнения diff. ).

2. В vue2 уменьшена гранулярность наблюдателя, и каждому компоненту соответствует только один наблюдатель, введен алгоритм diff для точного нахождения места, где происходит изменение.

3. Момент выполнения diff в vue — это когда экземпляр компонента выполняет свою функцию обновления, он сравнивает последний результат рендеринга oldVnode с новым результатом рендеринга newVnode, этот процесс мы называем исправлением.

4. Общая стратегия алгоритма сравнения: сравнение в глубину на одном уровне; сравнение между двумя узлами будет выполнять разные операции в зависимости от того, есть ли у них дочерние узлы или текстовые узлы; сравнение двух наборов дочерних узлов является ключевой частью алгоритм, т. е. сначала принять первый и последний Один и тот же узел можно сравнивать 4 раза. Если тот же узел не найден, будет выполнен обходной поиск. После завершения поиска оставшиеся узлы будут обработаны в соответствии с к ситуации; тот же узел можно быстро и точно найти с помощью ключа, поэтому весь процесс исправления очень эффективен.

Выше приведен алгоритм сравнения vue, надеюсь, он будет полезен для вашего понимания, укажите источник перепечатки, спасибо.

Для ключа vue вы можете обратиться к моей предыдущей статье: Анализ функции и принципа действия ключа в vue общих проблем vue (3) .

Алгоритм сравнения и ключ React можно найти в статье: Введение в алгоритм сравнения и ключевой атрибут React.

Guess you like

Origin blog.csdn.net/wh_xmy/article/details/109613131