Статья впервые опубликована на: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», фермера на гусиной фабрике, который обычно записывает какие-то банальные мелочи, технологии, жизнь, инсайты и срастается.