предисловие
Цель состоит в том, чтобы написать очень подробную сухую статью о diff, поэтому эта статья немного длинная. Он также будет использовать много изображений и примеров кода, давайте соберемся вместе.
Давайте сначала узнаем несколько вещей...
1. Как Vue обновляет узлы при изменении данных?
Вы должны знать, что накладные расходы на рендеринг реального DOM очень велики. Например, иногда мы изменяем определенные данные. Если они напрямую отображаются в реальном DOM, это вызовет перерисовку и перестановку всего дерева DOM. Возможно ли, что мы обновляем только измененные данные?А как насчет небольшого фрагмента дома вместо обновления всего дома? Алгоритм diff может нам помочь.
Сначала мы создаем его на основе реального DOM.virtual DOM
,когдаvirtual DOM
После изменения данных узла будет сгенерирован новыйVnode
,ПотомVnode
а такжеoldVnode
Для сравнения, если вы обнаружите, что есть разница, вы можете напрямую изменить ее в реальном DOM, а затем использоватьoldVnode
ценностьVnode
.
Процесс diff заключается в вызовеpatch
функцию, сравните старый и новый узлы и дайтенастоящий ДОМПластырь.
2. В чем разница между виртуальным DOM и реальным DOM?
Виртуальный DOM предназначен для извлечения реальных данных DOM и моделирования древовидной структуры в виде объектов. Например, дом выглядит так:
<div>
<p>123</p>
</div>
Соответствующий виртуальный DOM (псевдокод):
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};
(Советы:VNode
а такжеoldVNode
это все предметы, обязательно запомните)
3. Как сравнить diff?
При использовании алгоритма сравнения для сравнения старых и новых узлов сравнение будет выполняться только на одном уровне и не будет сравниваться между уровнями.
<div>
<p>123</p>
</div>
<div>
<span>456</span>
</div>
Приведенный выше код сравнивает два div на одном уровне и p и span на втором слое соответственно, но не сравнивает div и span. Очень яркая картина видна в другом месте:
блок-схема различий
Когда данные изменятся, метод set вызоветDep.notify
Оповестить всех подписчиков Watcher, абоненты будут звонитьpatch
Исправьте настоящий DOM и обновите соответствующие представления.
подробный анализ
patch
приди и посмотриpatch
Как это исправлено (код сохраняет только основную часть)
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
Функция patch принимает два параметраoldVnode
а такжеVnode
Представлять новый узел и предыдущий старый узел соответственно
- Определите, стоит ли сравнивать два узла, и выполните, если стоит сравнить
patchVnode
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
- Используйте, если не стоит сравнивать
Vnode
заменятьoldVnode
Если оба узла одинаковы, выполните детализацию, чтобы изучить их дочерние узлы. Если два узла различны, это означаетVnode
полностью изменен, его можно заменить напрямуюoldVnode
.
Хотя эти два узла не совпадают, что, если их дочерние узлы одинаковы? Не забывайте, диффы сравниваются слой за слоем, если первый слой отличается, он не будет продолжать сравнивать второй слой в глубину. (Мне интересно, является ли это недостатком? Одни и те же дочерние узлы нельзя использовать повторно...)
patchVnode
Когда мы определяем, что два узла заслуживают сравнения, мы указываем два узлаpatchVnode
метод. Так что же делает этот метод?
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
Эта функция делает следующее:
- Найдите соответствующий реальный дом, называемый
el
- судить
Vnode
а такжеoldVnode
Указывает ли он на один и тот же объект, если да, то прямоreturn
- Если они оба имеют текстовые узлы и не равны, то
el
Текстовый узел установлен наVnode
текстовый узел. - если
oldVnode
имеет дочерние узлыVnode
нет, удалитьel
дочерний узел - если
oldVnode
без дочерних узловVnode
Да, это будетVnode
Дочерние узлы добавляются вel
- Если у обоих есть дочерние узлы, выполните
updateChildren
Функция сравнивает детские узлы, этот шаг очень важен
С остальными моментами все понятно, подробнее поговорим об updateChildren.
updateChildren
Объем кода большой, и объяснять построчно неудобно, поэтому ниже я опишу его на нескольких примерах диаграмм.
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
, их 2 переменные сравниваются друг с другом, всего 4 метода сравнения. Если ни одно из 4 сравнений не совпадает, если установленоkey
, буду использоватьkey
Сравните, в процессе сравнения переменная окажется посередине, один разStartIdx>EndIdx
показыватьoldCh
а такжеvCh
По крайней мере один из них был пройден, и сравнение заканчивается.
Графическое обновление Дети
Наконец пришел к этой части, приведенное выше резюме считает, что многие люди также выглядят смущенными, давайте поговорим об этом. (Это все нарисовано мной, пожалуйста, порекомендуйте полезный инструмент для рисования...)
.
Розовые части - это oldCh и vCh
Мы удаляем их и используем указатели s и e, чтобы указать на их головной и хвостовой потомки соответственно.
теперь соответственноoldS、oldE、S、E
дважды дваsameVnode
Для сравнения есть четыре метода сравнения. Когда два из них могут совпасть, соответствующий узел в реальном доме переместится на соответствующую позицию Vnode. Это предложение немного сбивает с толку. Например,
- Если oldS и E совпадают, то первый узел в реальном доме будет перемещен в конец.
- Если oldE и S совпадают, то последний узел в реальном доме переместится вперед, а два указателя на совпадение переместятся в середину.
- Если ни одно из четырех совпадений не является успешным, возможны два случая.
- Если ключ существует и в старом, и в новом подузлах, то ключ будет
oldChild
ключ для создания хеш-таблицы, используйтеS
Сопоставьте ключ с хеш-таблицей и оцените успешность совпадения.S
и является ли соответствующий узелsameNode
, если это так, переместите успешный узел вперед в реальном доме, в противном случае переместите успешный узел впередS
Создайте соответствующий узел и вставьте его в соответствующий домoldS
Место нахождения,S
Указатель перемещается в середину, а узел в совпавшем старом становится нулевым. - Если нет ключа, напрямую
S
Создать новую вставку узла真实DOM
(ps: Это может объяснить, почему ключ нужно устанавливать при использовании v-for. Если ключа нет, будет сделано только четыре совпадения, даже если в середине указателя есть многоразовые узлы, их нельзя использовать повторно .)
- Если ключ существует и в старом, и в новом подузлах, то ключ будет
Затем настройте граф (при условии, что все узлы на рисунке ниже имеют ключи, а ключ является собственным значением)
- первый шаг
oldS = a, oldE = d;
S = a, E = b;
oldS
а такжеS
Если он совпадает, поместите узел a в дом как первый, и он будет проигнорирован, если он уже первый.В это время позиция дома: a b d
- второй шаг
oldS = b, oldE = d;
S = c, E = b;
oldS
а такжеE
Если он совпадает, переместите оригинал B узла в конец, потому чтоE
является последним узлом, их позиции должны быть одинаковыми, об этом сказано выше:Когда два из них могут совпасть, соответствующий узел в реальном доме будет перемещен в соответствующую позицию Vnode., положение dom в это время: a d b
- третий шаг
oldS = d, oldE = d;
S = c, E = d;
oldE
а такжеE
Совпадение, положение не меняется В это время положение дома: a d b
- четвертый шаг
oldS++;
oldE--;
oldS > oldE;
Обход заканчивается, указываяoldCh
Пройди сначала. оставшиесяvCh
Узел вставляется в реальный дом по своему индексу, в это время позиция дома следующая: a c d b
Делается симуляция.
Есть два условия для завершения этого процесса сопоставления:
-
oldS > oldE
выражатьoldCh
После обхода сначала, затем избыточногоvCh
Добавьте его в дом по индексу (как показано выше) -
S > E
Указывает, что сначала пройден vCh, затем в реальном dom интервал равен[oldS, oldE]
удалить лишние узлы
Вот еще один пример, вы можете попробовать смоделировать его самостоятельно, как показано выше.
когда эти узлыsameVnode
После успеха он будет выполнен немедленноpatchVnode
Теперь вы можете посмотреть на код выше
if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
}
Это продолжается рекурсивно до тех пор, пока не будут сравнены все дочерние узлы в oldVnode и Vnode. Все патчи дома тоже проигрываются. Не проще ли вернуться назад и посмотреть на код updateChildren сейчас?
Суммировать
Выше показан весь процесс алгоритма сравнения. Поместите итоговую картинку, отправленную в начале статьи. Вы можете попробовать посмотреть на эту картинку, чтобы вспомнить процесс сравнения.
Добро пожаловать, чтобы общаться больше в области комментариев.
Справочная статья