виртуальный дом
-
Почему появляется:
Браузерный анализ html примерно разделен на пять шагов: создать дерево DOM -> создать правила стиля -> построить дерево рендеринга -> макет макета -> нарисовать рисование. Каждый раз, когда работает настоящий DOM, браузер будет выполнять процесс от начала до конца, начиная с построения дерева DOM. Настоящая операция DOM стоит дорого, а частые операции также приведут к зависанию страницы и повлияют на взаимодействие с пользователем.Виртуальный дом был создан для решения этой проблемы с производительностью браузера.
После того, как виртуальный дом выполняет операцию обновления дома, виртуальный дом не работает напрямую с реальным домом, а сохраняет обновленное содержимое diff в локальном объекте js, а затем одновременно прикрепляет его к дереву дома, уведомляя браузер для рисования дом, чтобы избежать большого количества бесполезных вычислений.
-
Как достичь:
Объект js представляет структуру dom, а объект записывает метки, атрибуты и дочерние узлы узла dom.
Функция рендеринга объекта js строит реальное дерево дома, рекурсивно используя свойства и дочерние узлы виртуального дома.
Виртуальный DOM — это чистый JS-объект, к которому можно получить доступ через
document.createDocumentFragmentСоздайте, виртуальный DOM в Vue содержит следующие свойства:- tag: имя тега текущего узла
- data: объект данных текущего узла
- Children: тип массива, содержит дочерние узлы текущего узла
- текст: текст текущего узла, общие текстовые узлы или узлы комментариев будут иметь этот атрибут
- elm: реальный узел dom, соответствующий текущему виртуальному узлу
- контекст: область компиляции
- functionContext: область действия функционального компонента.
- ключ: ключевой атрибут узла, который используется в качестве идентификатора узла, что способствует оптимизации патча.
- SEL: селектор для узлов
- componentOptions: информация об опциях, которая будет использоваться при создании экземпляра компонента.
- дочерний элемент: экземпляр компонента, соответствующий текущему узлу
- родитель: узел-заполнитель компонента
- raw: raw html
- isStatic: идентификатор статического узла
- isRootInsert: следует ли вставлять в качестве корневого узла обернутый узел, значение этого свойства равно false
- isComment: является ли текущий узел узлом комментариев
- isCloned: является ли текущий узел клонированным узлом
- isOnce: есть ли у текущего узла команда v-once
Краткое резюме: Virtual DOM имитирует настоящие узлы DOM с помощью JavaScript и помещает сравнение изменений DOM в слой Js.
-
Преимущество:
- Кроссплатформенность: виртуальный DOM основан на объектах JavaScript и не зависит от среды реальной платформы, поэтому он имеет кроссплатформенные возможности, такие как платформы браузеров, Weex, Node и т. д.
- Повышение эффективности операций DOM: скорость выполнения операций DOM намного меньше, чем у Javascript, поэтому большое количество операций DOM переносится на Javascript, а алгоритм исправления используется для расчета узлов, которые действительно нуждаются в обновлении, поэтому чтобы минимизировать операции DOM, тем самым значительно улучшив производительность.
- Повышение производительности рендеринга: благодаря алгоритму сравнения представление может обновляться разумно и эффективно при больших и частых обновлениях данных.
Процесс преобразования шаблона в вид в vue
- Vue.js преобразует шаблон шаблона в функцию рендеринга (рендеринга) путем компиляции, а выполнение функции рендеринга может получить виртуальное дерево узлов
- Когда модель работает, объект Watcher в соответствующем Dep будет активирован. Объект Watcher вызовет соответствующее обновление для изменения представления. Этот процесс в основном предназначен для сравнения разницы между старыми и новыми виртуальными узлами (исправления), а затем выполнения операций DOM для обновления представления в соответствии с результатом сравнения.
Алгоритм diff — это метод оптимизации, в котором сравнивается разница между двумя модулями до и после, а процесс исправления (обновления) разницы называется исправлением.
patch:
Основная часть виртуального DOM, он может преобразовать vnode в реальный DOM.Сравните различия между старыми и новыми виртуальными узлами, а затем найдите узлы, которые необходимо обновить, по результатам сравнения..
Патч сам по себе означает исправление и исправление, и его фактическая функция состоит в том, чтобы изменить существующий DOM для достижения цели обновления представления. Алгоритм исправления Vue Virtual DOM основан наSnabbdomреализации, и внес множество корректировок и улучшений в эти основы.
процесс сравнения
Когда данные изменятся, метод set вызоветDep.notifyОповестить всех подписчиков Watcher, абоненты будут звонитьpatchИсправьте настоящий DOM и обновите соответствующие представления.
Алгоритм сравнения VueРазличайте только vnodes одного уровня, рекурсивно выполняйте diff vnodes одного уровня и, наконец, реализуйте обновление всего дерева DOM.. Поскольку операций в иерархии очень мало и ими можно пренебречь, временная сложность изменяется с O(n3) до O(n).
Предположения алгоритма сравнения
- Перемещение узлов DOM по иерархиям в веб-интерфейсе происходит очень редко и может быть проигнорировано.
- Два компонента с одним и тем же классом будут генерировать похожие древовидные структуры, а два компонента с разными классами будут генерировать разные древовидные структуры.
- Для группы дочерних узлов одного уровня их можно отличить по уникальному идентификатору.
процесс исправления
Когда ключ и sel старого и нового виртуальных узлов совпадают, выполняется патч глубины узла, если они не совпадают, заменяется весь виртуальный узел и одновременно создается реальный DOM для реализовать обновление представления.
Как определить, являются ли старый и новый узлы одним и тем же узлом:
Когда тег, ключ и isComment двух VNodes одинаковы, а данные определены или не определены одновременно, и если тег является входным, тип должен быть одинаковым. В это время два VNode рассматриваются как один и тот же Vnode, и операция patchVnode может выполняться напрямую.
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) { // 有必要进行patch, key和sel都相同时才进行patch
patchVnode(oldVnode, vnode)
} else { // 没有必要进行patch, 整个替换
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode) // vnode创建它的真实dom,令vnode.el =真实dom
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 插入整个新节点树
api.removeChild(parentEle, oldVnode.el) // 移出整个旧的虚拟DOM
oldVnode = null
}
}
return 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
1. Если старый и новый VNodes являются статическими, а их ключи одинаковыми (представляющими один и тот же узел), то необходимо заменить только elm и componentInstance (повторное использование на месте).
2. Если у старого и нового узлов есть дочерние узлы, и они разные, выполните операцию сравнения над дочерними узлами и вызовите updateChildren, этоupdateChildren также является ядром diff.
3. Если только новый узел имеет дочерние узлы, сначала очистите текстовое содержимое DOM старого узла, а затем добавьте дочерние узлы к текущему узлу DOM.
4. Если только старый узел имеет дочерние узлы, удалите дочерние узлы старого узла напрямую.
5. Когда старый и новый узлы не имеют дочерних узлов, это просто замена текста.
updateChildren
Далее идет понимание самого сложного алгоритма diff
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, 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
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Обзор процесса
- Буду
VnodeУзел ребенкаVchа такжеoldVnodeдочерний узелoldChизвлечен -
oldChа такжеvChПеременные с двумя головками и хвостамиStartIdxа такжеEndIdx, их две переменные сравниваются друг с другом, всего существует 4 метода сравнения, когда два из них могут совпасть, соответствующий узел в реальном доме будет перемещен в соответствующую позицию Vnode. Если ни одно из 4 сравнений не совпадает, если установленоkey, буду использоватьkeyСравните, в процессе сравнения переменная окажется посередине, один разStartIdx>EndIdxпоказыватьoldChа такжеvChПо крайней мере один из них был пройден, и сравнение заканчивается.
Слева и справа от нового и старого узлов VNode есть переменная метка, и эти переменные будут перемещаться ближе к середине в процессе обхода. Завершите цикл, когда oldStartIdx
Давайте разберем весь процесс сравнения на примере:
реальные узлы: а, б, г
старые узлы: а, б, г
новые узлы: а, в, г, б
первый шаг:
oldS = a, oldE = d;
S = a, E = b;
Сравните старое S с S, E; сравните старое E с S, E, получитеoldSа такжеSВ соответствии с выводом, узел должен быть помещен первым в порядке новых узлов. В это время узел a старого узла также является первым, поэтому положение не меняется;
В конце первого раунда сравнения oldS и S являются одним и тем же узлом, перемещаются назад, а oldE и E не перемещаются;
второй шаг:
старые узлы: а, б, г
новые узлы: а, в, г, б
oldS = b, oldE = d;
S = c, E = b;
Четыре переменные можно сравнить с двумя транспортными средствамиoldSа такжеEMatch, переместите исходный узел b в конец, потому чтоEявляется последним узлом, их позиции должны быть одинаковыми, об этом сказано выше:Когда два из них могут совпасть, соответствующий узел в реальном доме будет перемещен в соответствующую позицию Vnode.;
Второй раунд сравнения окончен, oldE и E — один и тот же узел, продвигаемся вперед, а oldS и S остаются без изменений;
третий шаг:
старые узлы: а, г, б
новые узлы: а, в, г, б
oldS = d, oldE = d;
S = c, E = d;
oldEа такжеEсовпадение, положение без изменений;
четвертый шаг:
старые узлы: а, г, б
новые узлы: а, в, г, б
oldS++;
oldE--;
oldS > oldE;
Обход завершен, что указывает на то, что старый узел был пройден первым. Вставьте оставшийся новый узел c в реальный дом в соответствии с его собственным индексом.
старые узлы: а, в, г, б
новые узлы: а, в, г, б
Сравнение завершено.
Конечно, будетЧетыре переменные не могут соответствовать друг другу, в двух случаях
- Если старые и новые дочерние узлыключ существует, то будет сгенерирована хеш-таблица на основе ключа старого узла, используя
SСопоставьте ключ с хеш-таблицей и оцените успешность совпадения.Sи является ли соответствующий узелsameNode, если это так, переместите успешный узел вперед в реальном доме, в противном случае переместите успешный узел впередSСоздайте соответствующий узел и вставьте его в соответствующий домoldSМесто нахождения,oldSа такжеSУказатель перемещается в середину. - Если нет ключа, напрямую
SСоздать новую вставку узла真实DOM(Здесь можно объяснить, почему установка ключа делает diff более эффективным
В конце есть два конкретных случая:
-
oldS > oldE, можно считать, что старый узел пройден первым. Конечно, также возможно, что новый узел только что завершил обход в это время, и он весь относится к этой категории. В это время добавляется vnode между S и E, вызовите addVnodes и вставьте все эти виртуальные node.elm в конец предыдущего. -
S> EЭто можно считать новым узлом, впервые пройденным. В это время узел между старыми и Олд в новом узере дочернего ребенка еще не существует, удалить
Опыт моделирования двух примеров
eg.1
O b,a,d,f,e
N a,b,e
1.
oldS = b, oldE = e;
S = a, E = e;
O b,a,d,f,e
N a,b,e
2.
oldS = b, oldE = f;
S = a, E = b;
O a,d,f,b,e
N a,b,e
3.
s>e d,f 删除
O a,b,e
N a,b,e
eg.2
O b,d,c,a
N a,e,b,f
1.
oldS = b, oldE = a;
S = a, E = f;
O a,b,d,c
N a,e,b,f
2.
oldS = b, oldE = c;
S = e, E = f;
此时四个参数无法匹配,根据key来对比O中是否有S对应的节点,没有,则在O的S位置插入对应节点
O a,e,b,d,c
N a,e,b,f
3.
oldS = b, oldE = c;
S = b, E = f;
olds与s对应,原地不动
O a,e,b,d,c
N a,e,b,f
4.
oldS = d, oldE = c;
S = f, E = f;
此时四个参数无法匹配,根据key查找是否有S对应的f节点,没有,则在O的S位置插入对应节点
O a,e,b,d,c,f
N a,e,b,f
5.
oldS = d, oldE = c;
s>f
循环结束,oldS与oldE之间的节点删除
Суммировать:
- Старайтесь не изменять дом на разных уровнях
- При разработке компонентов поддержание стабильной структуры DOM поможет повысить производительность.
- Ключ настройки может сделать diff более эффективным
Ссылаться на:
Подробно объясните виртуальный DOM в Vue.
Виртуальный DOM и различия (реализовано Vue)