Процесс отображения Vue.js из виртуального DOM в реальный DOM

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

Статья впервые опубликована на:GitHub.com/US TB-Вуд, умри, о ты…

написать впереди

Считается, что понятие Virtual DOM знакомо всем, поскольку виртуальный DOM связан с DOM (объектной моделью документа).MDNОб определении DOM: «Модель DOM использует логическое дерево для представления документа. Конечная точка каждой ветви дерева является узлом (узлом), и каждый узел содержит объекты (объекты). Методы DOM (методы) Позволяет вам управлять деревом определенным образом, в котором вы можете изменить структуру, стиль или содержание документа». По сравнению с проблемами производительности, вызванными частыми операциями с DOM, Virtual DOM хорошо справляется с отображением DOM и отображает ряд операций, которые изначально необходимо было выполнять в DOM для работы с Virtual DOM.

«Дорогой» ДОМ

Чтобы иметь более интуитивное представление о «дорогом» DOM, теперь распечатайте все значения атрибутов простого элемента div:

let div = document.createElement('div')
let str = ''
for (let key in div) {
	str += key + ' '
}

Напечатанное значение str:

Видно, что настоящие элементы DOM очень большие, потому что браузер проектирует DOM очень сложно, поэтому, когда мы часто обновляем DOM, возникают определенные проблемы с производительностью. Можно изменить всю DOM-структуру с помощью innerHTML на странице простым и грубым способом, а перерисовка всего слоя представления таким образом довольно требовательна к производительности. Когда мы обновляем DOM, можем ли мы обновлять только измененные места?

VNode

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

Давайте посмотрим на определение VNode в исходном коде Vue.js, которое определено в 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; // rendered in this component's scope
  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
  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
  }
}

в:

tag: Имя метки текущего узла

data: объект, соответствующий текущему узлу, который содержит определенную информацию о данных, является типом VNodeData, вы можете ссылаться на информацию о данных в типе VNodeData.

children: дочерние узлы текущего узла, который представляет собой массив

text: текст текущего узла

elm: узел реального дома, соответствующий текущему виртуальному узлу.

ns: пространство имен текущего узла

context: область компиляции текущего узла

functionalContext: область действия функционального компонента

key: ключевой атрибут узла, который используется в качестве флага узла для оптимизации.

componentOptions: опции опции компонента

componentInstance: Экземпляр компонента, соответствующий текущему узлу.

parent: родительский узел текущего узла

raw: Короче говоря, будь то собственный HTML или обычный текст, true для innerHTML, false для textContent.

isStatic: Является ли это статическим узлом

isRootInsert: вставлять ли как подчиненный узел

isComment: это узел комментариев

isCloned: Является ли это клонированным узлом

isOnce: Есть ли команда v-once

Например, теперь у нас есть такой виртуальный DOM:

 {
    tag: 'div'
    data: {
        class: 'outer'
    },
    children: [
        {
            tag: 'div',
            data: {
                class: 'inner'
            }
            text: 'Virtual DOM'
        }
    ]
}

Настоящий DOM после рендеринга:

<div class="outer">
    <span class="inner">Virtual DOM</span>
</div>

Создайте пустой узел VNode

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

Создайте текстовый узел

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

Клонировать виртуальный узел

export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

В общем, VNode — это объект JavaScript, свойства объекта JavaScript используются для описания некоторых состояний текущего узла, а дерево Virtual DOM моделируется в виде узлов VNode.

обновить представление

Мы знаем, что Vue.js обновляет представление через привязку данных, которая вызывает метод updateComponent.Если вы не понимаете этот процесс, вы можете прочитать две статьи, упомянутые выше. Метод updateComponent определяется следующим образом:

updateComponent = () => {
 vm._update(vm._render(), hydrating)
}

Этот метод вызовет метод vm._update.Первым параметром, принимаемым этим методом, является вновь сгенерированный VNode, который определен в src/core/instance/lifecycle.js:

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    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.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // 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.
  }

Среди них добавьте комментарии в ключевых местах:

  // 新的vnode
  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
   // 第一个参数为真实的node节点
   vm.$el = vm.__patch__(
    vm.$el, vnode, hydrating, false /* removeOnly */,
    vm.$options._parentElm,
    vm.$options._refElm
   )
  } else {
   // updates
   // 如果需要diff的prevVnode存在,那么首先对prevVnode和vnode进行diff,并将需要的更新的dom操作已patch的形式打到prevVnode上,并完成真实dom的更新工作
   vm.$el = vm.__patch__(prevVnode, vnode)
  }

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

patch

Далее давайте посмотрим, что случилось с методом vm.__patch__, который определен в src/core/vdom/patch.js:

  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    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
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      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)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              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
          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)) {
          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)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

Из исходного кода мы можем обнаружить, что patchVnode будет выполняться только тогда, когда oldVnode (старый узел) и vnode (новый узел) находятся в sameVnode.Метод sameVnode определяет, выполнять ли процесс сравнения и исправления на oldvnode и vnode. То есть процесс patchVnode будет выполняться только тогда, когда старый и новый узлы VNode будут определены как один и тот же узел, в противном случае будет создан новый DOM, а старый DOM будет удален. Тот же метод Vnode описан ниже:

sameVnode

sameVnode определен в src/core/vdom/patch.js:

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)
      )
    )
  )
}

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)
}

Код можно увидеть только тогда, когда старый и новый тег VNode, ключ, isComment совпадают, в то же время, когда данные определены или не определены, и если входной тег должен быть одного типа. В этот раз считается, что оба новых VNode совпадают с Vnode, после чего выполняются операции patchVnode.

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

Алгоритм vdom Vue в версии 2.x реализован на основе модификаций, сделанных алгоритмом snabbdom.

Как показано на рисунке, алгоритм сравнения сравнивает узлы дерева одного и того же слоя вместо поиска и обхода дерева слой за слоем, поэтому временная сложность составляет всего O(n), что является очень эффективным алгоритмом. Далее давайте взглянем на реализацию исходного кода updateChildren, самой важной части алгоритма сравнения.

updateChildren

Определение исходного кода updateChildren находится в src/core/vdom/patch.js:

  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)) {
        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 {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      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)
    }
  }

Анализ исходного кода этой части может относиться кизучение исходного кода snabdom.

Можете обратить внимание на мой паблик-аккаунт «Muchen Classmate», фермера на гусиной фабрике, который обычно записывает какие-то банальные мелочи, технологии, жизнь, инсайты и срастается.