Подробно объясните алгоритм сравнения vue.

Vue.js
Подробно объясните алгоритм сравнения vue.

предисловие

Цель состоит в том, чтобы написать очень подробную сухую статью о 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 сейчас?

Суммировать

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

Добро пожаловать, чтобы общаться больше в области комментариев.

Справочная статья

Разобрать алгоритм сравнения vue2.0

Виртуальный DOM и различия (реализовано Vue)

PS: ByteDance набирает внешний интерфейс, внутренний push-код: WAU8ZHR, вы также можете оставить сообщение, чтобы помочь проголосовать, добро пожаловать в доставку ~