Предыдущая глава представилаVNodeкак создать настоящийDom, это толькоpatchЧто он делает для первого рендеринга страницы, так это завершает небольшую часть функции, и самое главное, что он делает, это эффективно завершает процесс повторного рендеринга страницы, когда срабатывает отзывчивость. На самом деле, повторный рендеринг страницы может полностью использовать только что сгенерированныйDomпойди поменяй старыйDom, однако это неэффективно, поэтому использование следующегоdiffСравнение алгоритмов сделано.
diffАлгоритм сравниваетVNodeа такжеoldVNode, затем сVNodeв случае стандартаoldVNodeВнесите небольшие изменения вVNodeсоответствующийDomоказывать.
назад до_updateРеализация метода, на этот раз придет кelseЛогика такова:
Vue.prototype._update = function(vnode) {
const vm = this
const prevVnode = vm._vnode
vm._vnode = vnode // 缓存为之前vnode
if(!prevVnode) { // 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode)
} else { // 重新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
Поскольку существующийVNodeЭто переделка для достижения цели повторного рендеринга, поэтому это не более чем три вещи:
Создать новый узел
удалить устаревшие узлы
Обновите существующие узлы
Далее мы представим ситуации, в которых встречаются вышеупомянутые три ситуации.
Создать новый узел
При добавлении нового узла возможны две ситуации:
VNodeнекоторые узлы иoldVNodeнет
-
VNodeнекоторые узлы иoldVNodeНет, самая очевидная сцена — это первый рендер, в этот раз нетoldVNode, так что весьVNodeсделать как реальныйDomВставьте в корневой узел, этот подробный процесс подробно описан в предыдущей главе.
VNodeа такжеoldVNodeполностью отличается
- когда
VNodeа такжеoldVNodeКогда это не один и тот же узел, он будет напрямуюVNodeСоздан как настоящийDom, вставленный после старого узла. В это время старый узел становится заброшенным узлом и удаляется для завершения процесса замены.
Чтобы определить, являются ли два узла одним и тем же узлом, внутреннее определение выглядит следующим образом:
function sameVnode (a, b) { // 是否是相同的VNode节点
return (
a.key === b.key && ( // 如平时v-for内写的key
(
a.tag === b.tag && // tag相同
a.isComment === b.isComment && // 注释节点
isDef(a.data) === isDef(b.data) && // 都有data属性
sameInputType(a, b) // 相同的input类型
) || (
isTrue(a.isAsyncPlaceholder) && // 是异步占位符节点
a.asyncFactory === b.asyncFactory && // 异步工厂方法
isUndef(b.asyncFactory.error)
)
)
)
}
удалить устаревшие узлы
Второй случай создания нового узла выше упоминается незначительно, сравнитеvnodeа такжеoldVnode, если корневой узел не тот, он будетVnodeЦелое представлено как реальноеDom, вставьте его после старого узла и, наконец, удалите старый узел, от которого отказались:
patchметод создастDomПосле вставки после отброшенного узла:
if (isDef(parentElm)) { // 在它们的父节点内删除旧节点
removeVnodes(parentElm, [oldVnode], 0, 0)
}
-------------------------------------------------------------
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
removeNode(ch.elm)
}
}
} // 移除从startIdx到endIdx之间的内容
------------------------------------------------------------
function removeNode(el) { // 单个节点移除
const parent = nodeOps.parentNode(el)
if(isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
Обновление существующих узлов (выделено)
ЭтоdiffКлючевым моментом алгоритма является то, что когда два узла являются одним и тем же узлом, необходимо в это время выяснить их отличия, сравнение которых осуществляется в основном с помощьюpatchVnodeметод, этот метод в основном имеет дело с несколькими отраслевыми ситуациями:
являются статическими узлами
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) { // 完全一样
return
}
const elm = vnode.elm = oldVnode.elm
if(isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic)) {
vnode.componentInstance = oldVnode.componentInstance
return // 都是静态节点,跳过
}
...
}
Что такое статический узел? Это то, что делает фаза компиляции, она находит статические узлы в шаблоне и помечает их (isStaticдляtrue),Например:
<template>
<div>
<h2>{{title}}</h2>
<p>新鲜食材</p>
</div>
</template>
здесьh2Метки не являются статическими узлами, потому что они изменяются на основе интерполяции, в то время какpМетки являются статическими узлами, потому что они не изменяются. Если все они являются статическими узлами, пропустите это сравнение, которое также является этапом компиляции.diffОптимизация сравнения.
vnodeУ узла нет текстового атрибута
function patchVnode(oldVnode, vnode) {
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) { // vnode没有text属性
if (isDef(oldCh) && isDef(ch)) { // // 都有children
if (oldCh !== ch) { // 且children不同
updateChildren(elm, oldCh, ch) // 更新子节点
}
}
else if (isDef(ch)) { // 只有vnode有children
if (isDef(oldVnode.text)) { // oldVnode有文本节点
nodeOps.setTextContent(elm, '') // 设置oldVnode文本为空
}
addVnodes(elm, null, ch, 0, ch.length - 1)
// 往oldVnode空的标签内插入vnode的children的真实dom
}
else if (isDef(oldCh)) { // 只有oldVnode有children
removeVnodes(elm, oldCh, 0, oldCh.length - 1) // 全部移除
}
else if (isDef(oldVnode.text)) { // oldVnode有文本节点
nodeOps.setTextContent(elm, '') // 设置为空
}
}
else { vnode有text属性
...
}
...
еслиvnodeБез текстового узла есть следующие четыре ветви:
1. иметь обаchildrenи не то же самое
- использовать
updateChildrenметоды сравнивают ихchildren, если обновление существующего узлаpatchядро, то вот обновлениеchildrenЭто ядро ядра, которое подробно объясняется позже с помощью блок-схемы.
2. Толькоvnodeимеютchildren
- что здесь
oldVnodeЛибо пустая метка, либо текстовый узел, если это текстовый узел, очистите текстовый узел, а затемvnodeизchildrenСоздан как настоящийDomвставляется в пустой тег.
3. ТолькоoldVnodeимеютchildren
- потому что это так
vnodeявляется стандартным, поэтомуvnodeничего,oldVnodeВнутри находится заброшенный узел, который нужно удалить.
4. ТолькоoldVnodeТекст
- только если
oldVnodeесть иvnodeНет, просто очистите или удалите его.
vnodeУзлы имеют текстовые свойства
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) { // vnode没有text属性
...
} else if(oldVnode.text !== vnode.text) { // vnode有text属性且不同
nodeOps.setTextContent(elm, vnode.text) // 设置文本
}
...
снова, сvnodeстандартный, такvnodeЕсли есть текстовые узлы, неважноoldVnodeКакой это тип узла, прямо установленный наvnodeтекст внутри. До сих пор весьdiffДаже если общий процесс сравнения завершен, мы все равно используем блок-схему для пояснения идеи:
Обновите дочерний узел обновления существующего узла (акцент в главном)
更新子节点示例:
<template>
<ul>
<li v-for='item in list' :key='item.id'>{{item.name}}</li>
</ul>
</template>
export default {
data() {
return {
list: [{
id: 'a1',name: 'A'}, {
id: 'b2',name: 'B'}, {
id: 'c3',name: 'C'}, {
id: 'd4',name: 'D'}
]
}
},
mounted() {
setTimeout(() => {
this.list.sort(() => Math.random() - .5)
.unshift({id: 'e5', name: 'E'})
}, 1000)
}
}
В приведенном выше коде сначала отображается список, затем случайным образом перетасовывается и добавляется элемент в начало списка.В это время будет запущена логика компонента для обновления дочерних узлов, и будет какая-то другая логика раньше, здесь используйте только Обратите внимание на обновление связанного с подузлом, давайте посмотрим, как он обновляетсяDomиз:
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0 // 旧第一个下标
let oldStartVnode = oldCh[0] // 旧第一个节点
let oldEndIdx = oldCh.length - 1 // 旧最后下标
let oldEndVnode = oldCh[oldEndIdx] // 旧最后节点
let newStartIdx = 0 // 新第一个下标
let newStartVnode = newCh[0] // 新第一个节点
let newEndIdx = newCh.length - 1 // 新最后下标
let newEndVnode = newCh[newEndIdx] // 新最后节点
let oldKeyToIdx // 旧节点key和下标的对象集合
let idxInOld // 新节点key在旧节点key集合里的下标
let vnodeToMove // idxInOld对应的旧节点
let refElm // 参考节点
checkDuplicateKeys(newCh) // 检测newVnode的key是否有重复
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 开始遍历children
if (isUndef(oldStartVnode)) { // 跳过因位移留下的undefined
oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) { // 跳过因位移留下的undefine
oldEndVnode = oldCh[--oldEndIdx]
}
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) // 递归调用
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// 将旧第一节点右移到最后,视图立刻呈现
oldStartVnode = oldCh[++oldStartIdx] // 旧开始节点被处理,旧开始节点为第二个
newEndVnode = newCh[--newEndIdx] // 新最后节点被处理,新最后节点为倒数第二个
}
else if (sameVnode(oldEndVnode, newStartVnode)) { // 比对旧最后和新第一节点
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 递归调用
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 将旧最后节点左移到最前面,视图立刻呈现
oldEndVnode = oldCh[--oldEndIdx] // 旧最后节点被处理,旧最后节点为倒数第二个
newStartVnode = newCh[++newStartIdx] // 新第一节点被处理,新第一节点为第二个
}
else { // 不包括以上四种快捷比对方式
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 获取旧开始到结束节点的key和下表集合
}
idxInOld = isDef(newStartVnode.key) // 获取新节点key在旧节点key集合里的下标
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // 找不到对应的下标,表示新节点是新增的,需要创建新dom
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
)
}
else { // 能找到对应的下标,表示是已有的节点,移动位置即可
vnodeToMove = oldCh[idxInOld] // 获取对应已有的旧节点
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}
newStartVnode = newCh[++newStartIdx] // 新开始下标和节点更新为第二个节点
}
}
...
}
Функция сначала определит кучуletопределенные переменные, которые связаны сwhileЕсли тело цикла изменяет текущее значение, условием выхода из цикла является выход до тех пор, пока обрабатывается один из старых и новых списков узлов.Код тела цикла довольно сложен.На самом деле он делает только три вещи .Поняв три вещи, посмотрите на тело цикла, вы обнаружите, что это не сложно:
1. пропустить неопределенное
почему бы не бытьundefined, следующая блок-схема ясно объяснит. Просто помните, что если старый начальный узелundefined, переместиться назад на единицу; если старый конечный узелundefined, продвиньтесь на одно место.
2. Быстрый поиск
Сначала будут опробованы четыре метода быстрого поиска, при отсутствии совпадений будет выполнена дальнейшая обработка:
- 2.1 Сравнение новых стартовых и старых стартовых узлов
Если они совпадают, значит, их позиции верны,DomМенять не нужно, просто переместите нижний индекс старого и нового узлов на одно место.
- 2.2 Сравнение старых и новых конечных узлов
Если они совпадают, это также означает, что их позиции верны,DomМенять не нужно, просто переместите нижний индекс конца старого и нового узлов вперед на единицу.
- 2.3 Сравнение старых начальных и новых конечных узлов
Если он совпадает, местоположение указано неверно и его необходимо обновить.Domвид, старый начальный узел, соответствующий реальномуDomВставленный до последней цифры, старый индекс начального узла перемещается на единицу назад, а индекс нового конечного узла перемещается на единицу вперед.
- 2.4 Сравнение старых конечных и новых начальных узлов
Если он совпадает, местоположение указано неверно и его необходимо обновить.Domвид, старый конечный узел, соответствующий реальномуDomВставить в старый начальный узел, соответствующий истинеDomПеред , нижний индекс старого конечного узла смещается вперед на единицу, а нижний индекс нового начального узла смещается на один назад.
3. Поиск ключ-значение
- 3.1 Если он соответствует существующему ключевому значению
Это означает, что это существующий узел, но позиция неверна, затем переместите позицию узла.
- 3.2 Если он не соответствует существующему значению ключа
существующийkeyЕсли он не найден в наборе значений, значит, это новый узел, тогда создайте соответствующий реальный узелDomузел, вставленный в реальный, соответствующий старому начальному узлуDomТолько впереди.
Это не очень легко понять.В сочетании с предыдущим примером следующая блок-схема многое поймет:
startа такжеendотметка.
keyПоиск в списке значений, описание не найденоEэто новый узел, который создает соответствующий реальныйDom, вставленный в старый узелstartсоответствует действительностиDomнапротивAПеред , один был обработан, новыйstartСдвиньте позицию назад на одну позицию.
keyСписок значений поиска. Обнаружено, что это существующий узел, но позиция неправильная, затем выполняется операция вставки, а опорный узел по-прежнемуAузел, исходный старый узелCУстановить какundefined, который будет пропущен после этого. После обработки другого узла новыйstartВернитесь на одно место.
DomРасположение правильное, новоеstartи старыйstartВсе сдвинулось на одно место.
DomПозиция неверна, вставьте узел в последнюю позицию и, наконец, вставьте новыйendсдвинься вперед на одно место, старыйstartВернитесь на одно место.
undefinedлогика, а затем запустите быстрое сравнение, новый начальный узел и старый начальный узел сопоставляются, иstartСдвиньтесь на один бит назад, и на этот раз он выйдет из цикла. Затем посмотрите на окончательный код закрытия:
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0
...
while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
...
}
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) // 删除废弃节点
}
}
Наш предыдущий пример представляет собой цикл, в котором старые и новые списки узлов обрабатываются одновременно, а вот другая обработка для узлов, которые не были обработаны после выхода из цикла:
Domи вставьте его в представление. это всеdiffПроцесс алгоритма завершен, можете сравнить предыдущую рекурсивную блок-схему и прочитать ее еще раз, я думаю, что идея будет намного яснее.
Наконец, как обычно, мы по-прежнему используемvueВопросы для интервью, которые можно задать в конце этой главы~
Интервьюер улыбнулся и вежливо спросил:
- Зачем
v-forРекомендуется связать каждый элементkey, и желательно уникальный, не рекомендуетсяindex?
Дайте отпор:
- существует
diffПри сравнении внутренних подузлов обновления оно будет основываться наoldVnodeУзлы, которые не обрабатываются внутри, получаютkeyКоллекция объектов, соответствующих значениям и индексам, для при обработкеvnodeВ каждом узле он может быстро узнать, является ли узел существующим узлом, тем самым улучшая общуюdiffсравнительная производительность. Если это динамический список,keyЛучше всего оставить значение уникальным, но для списка, который не меняется, как карусель, используйтеindexЭто тоже хорошо.
Просто нажмите «Нравится» или подпишитесь, это легко найти~
Ссылаться на:
Всесторонний углубленный анализ исходного кода Vue.js
Vue.js объясняется простым способом
Поделитесь библиотекой компонентов со всеми, вы можете ею пользоваться ~ ↓