Демистификация процесса создания Vue реального DOM из Virtual DOM

Vue.js

1. Введение в виртуальный DOM

настоящий ДОМ

   Как видно из рисунка выше, реальный элемент DOM очень большой, это связано с тем, что стандарт браузера делает DOM очень сложным (объект DOM содержит множество свойств, как показано на рисунке выше). Когда мы делаем частые обновления DOM, соответственно возникают проблемы с производительностью.

Виртуальный виртуальный дом

   Чтобы решить проблему производительности часто используемого DOM, родился Virtual DOM. Виртуальный виртуальный DOM должен использовать собственный объект JS для описания узла DOM. Следовательно, это намного дешевле, чем создание реального DOM.

2. Процесс перехода от виртуального DOM к реальному DOM (Vue)

1. Определите виртуальный узел

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

Свойства объектов VNode

   Давайте посмотрим на исходный код версии Vue.js 2.x Что касается определения VNode, объект VNode определяет следующие свойства:

в файле src/core/vdom/vnode.js

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void;
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void;  component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

Объект VNode содержит следующие свойства:

  • tag: Имя метки текущего узла
  • data: объект данных текущего узла, поля которого могут ссылаться на определение VNodeData в исходном коде Vue types/vnode.d.ts.
  • children: тип массива, дочерние узлы текущего узла
  • text: текст текущего узла
  • elm: реальное значение, соответствующее текущему виртуальному узлу.
  • ns: Пространство имен пространства имен текущего узла
  • context: область компиляции текущего узла
  • key: Ключевой атрибут узла используется в качестве идентификатора узла, что способствует оптимизации патча.
  • componentOptions: информация о параметрах, которая будет использоваться для создания экземпляра компонента.
  • componentInstance: Экземпляр компонента, соответствующий текущему узлу
  • parent: родительский узел текущего узла
  • raw: определить, является ли это HTML или обычный текст, true, если innerHTML, false, если innerText
  • isStatic: ID статического узла
  • isRootInsert: Вставлять ли в качестве корневого узла обернутый узел, значение этого атрибута равно false
  • isComment: Является ли текущий узел узлом комментариев
  • isCloned: Является ли текущий узел узлом-клоном
  • isOnce: Есть ли команда v-once

...

Классификация VNodes

  • EmptyNode: узел комментария без содержимого
  • TextVNode: текстовый узел
  • ElementVNode: Узел обычного элемента
  • ComponentVNode: узел компонента
  • CloneVNode: Клонировать узел, который может быть любым из вышеперечисленных типов узлов, с той лишь разницей, что атрибут isCloned имеет значение true.

...

2. создатьЭлемент

файл src/core/vdom/create-element.js

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

// wrapper function for providing a more flexible interface
// without getting yelled at by flow
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 兼容不传data的情况
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // 调用_createElement创建虚拟节点
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 判断是否是__ob__响应式数据,不允许VNode是响应式data
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode() // 返回一个注释节点
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  // 当组件的is属性被设置为falsy的值
  // 创建一个没有内容的注释节点
  if (!tag) {
    return createEmptyVNode()
  }
  
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根据normalizationType的值,选择不同的处理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)// 对多层嵌套的children处理,返回一维数组
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)// 对只有一级children做处理,返回一维数组
  }
  let vnode, ns
  // 判断tag是否是字符串类型
  if (typeof tag === 'string') {
    let Ctor
    // 配置标签名的命名空间
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 判断tag是否是HTML的保留标签
    if (config.isReservedTag(tag)) {
      // 是保留标签,创建保留标签的VNode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    // 判断tag是否是component组件
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 是组件标签,创建一个componentVNode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 兜底方案,创建一个空的注释节点
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

Логика   createElement представлена ​​в следующей блок-схеме:Этап   createElement заключается в преобразовании всех дочерних элементов в однобитный массив, что удобно для последующих операций.

3, обновление

_update в Vue — это закрытый метод, который вызывается два раза, один раз при первом рендеринге, а другой — при обновлении данных. Давайте посмотрим на первый рендеринг, вызывая метод updateComponent, код выглядит следующим образом:

в файле src/core/instance/lifecycle.js

updateComponent = () => {
    vm._update(vm._render(), hydrating)
}
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // 如果需要diff的prevVnode不存在,那么就用新的vnode创建一个真实dom节点
    if (!prevVnode) {
      // initial render
      // $el参数为真实的dom节点
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      // prevVnode存在,传入prevVnode和vnode进行diff,完成真实dom的更新工作
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    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.
 }

Из приведенного выше исходного кода метод _update вызывает основной метод__patch__, который можно назвать основным методом для всего виртуального DOM для создания реального DOM. Он в основном завершает процесс сравнения нового виртуального узла и старого виртуального узла.После процесса исправления создается реальный узел DOM, и обновление представления завершается.

4. Патч

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

алгоритм сравнения

   Давайте сначала разберемся в алгоритме diff.Этот алгоритм сравнивает узлы дерева одного слоя вместо поиска и обхода дерева слой за слоем, поэтому временная сложность составляет всего O(n), а производительность достаточно эффективна. Вышеприведенные два изображения представляют собой процесс сравнения старого и нового VNode с использованием алгоритма diff.Они изменяются только при сравнении одного и того же слоя (один и тот же цветной квадрат на втором изображении представляет узлы VNode, которые сравниваются друг с другом) , а затем измените измененный вид. Блок модификации небольшой, поэтому он очень эффективен.
   Взглянем на исходный код патча:

файл src/core/vdom/patch.js

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 {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // 是同一个节点的时候,直接修改现有的节点
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, 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失败,创建一个空节点
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        // 虚拟节点创建真实的 DOM 并插入到它的父节点中
        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) {
              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
}

  Из кода патча несложно узнать, что patchVnode будет вызываться только тогда, когда oldVnode и vnode находятся на одном узле с sameVnode, иначе будет создан новый DOM, а старый DOM будет удален.
Правила для   patchVnode следующие:
  1) Если oldVnode и vnode точно совпадают, то ничего делать не нужно.
2) Если oldVnode и vnode являются статическими узлами и имеют один и тот же ключ, когда vnode является узлом-клоном или узлом, управляемым инструкцией v-once, вам нужно только скопировать oldVnode.elm и oldVnode.child в vnode, а также Никаких дальнейших действий не требуется.
  3) Если и у старых, и у новых узлов есть дочерние дочерние узлы, выполните операцию diff на дочерних узлах и вызовите updateChildren, который также является ядром diff.
  4) Если у старого узла нет дочерних узлов, а у нового узла есть дочерние узлы, сначала очистите текстовое содержимое DOM старого узла, а затем добавьте дочерние узлы к текущему узлу DOM.
  5) Когда у нового узла нет дочерних узлов, а у старого узла есть дочерние узлы, непосредственно удалите все дочерние узлы узла DOM.
  6) Когда старые и новые узлы не имеют дочерних узлов, это просто замена текста.
   Давайте посмотрим на ядро ​​функции 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, 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)) {
        // 如果oldStartVnode和newStartVnode是同一个VNode,递归调用patchVnode
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]// oldStartIdx向右移动
        newStartVnode = newCh[++newStartIdx]// newStartIdx向右移动
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 如果oldEndVnode,newEndVnode是同一个VNode,递归调用patchVnode
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]// oldEndIdx向左移动
        newEndVnode = newCh[--newEndIdx]// newEndIdx向左移动
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 如果oldStartVnode和newEndVnode是同一个VNode,递归调用patchVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]// oldStartIdx向右移动
        newEndVnode = newCh[--newEndIdx]// newEndIdx向左移动
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 如果oldEndVnode和newStartVnode是同一个VNode,递归调用patchVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]// oldEndIdx向右移动
        newStartVnode = newCh[++newStartIdx]// 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, newCh, newStartIdx)
            oldCh[idxInOld] = undefined
            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)
    }
}

Прочитав исходный код Updatechildren, идея алгоритма все еще немного расплывчато, поэтому давайте будем использовать диаграмму, чтобы выяснить идею:

  • Во-первых, имеется переменный маркер в старом и новом левом и правом краниокаудальных узлах 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.

- Если oldStartIdx и newEndIdx удовлетворяют условиям sameVnode, т.е. sameVnode(oldStartVnode, newEndVnode). В настоящее время это означает, что oldStartVnode работает после oldEndVnode.Когда patchVnode выполняется, реальный узел DOM должен быть перемещен за oldEndVnode.- Если oldEndIdx и newStartIdx удовлетворяют sameVnode, т.е. sameVnode(oldEndVnode, newStartVnode). Это показывает, что oldEndVnode располагался впереди oldStartVnode, а реальный узел DOM перемещался впереди oldStartVnode во время выполнения patchVnode.

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

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

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

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

резюме

  Виртуальный DOM прошел ключевые этапы createElement для создания VNode, обновления обновления представления, исправления для сравнения старых и новых виртуальных узлов и создания элементов DOM перед созданием реального DOM. Функция patch сравнивает старый и новый VNodes и использует алгоритм diff.Идея его алгоритма исходит из snabbdom.Если вам интересно, вы можете изучить дальшеУзнать исходный код Snabbdom~~