共计 14230 个字符,预计需要花费 36 分钟才能阅读完成。
最近温习到虚构 DOM 与 Diff,翻阅了泛滥材料,特此总结了这篇长文,加深本人对 vue 的了解。这篇文章比拟具体的剖析了 vue 的虚构 DOM,Diff 算法,其中一些要害的中央从别处搬运了一些图进行阐明(感激制图的大佬),也蕴含比拟具体的源码解读。
实在 DOM 的渲染
在讲虚构 DOM 之前,先说一下实在 DOM 的渲染。
浏览器实在 DOM 渲染的过程大略分为以下几个局部
- 构建 DOM 树。通过 HTML parser 解析解决 HTML 标记,将它们构建为 DOM 树(DOM tree),当解析器遇到非阻塞资源(图片,css), 会持续解析,然而如果遇到 script 标签(特地是没有 async 和 defer 属性),会阻塞渲染并进行 html 的解析,这就是为啥最好把 script 标签放在 body 上面的起因。
- 构建 CSSOM 树。与构建 DOM 相似, 浏览器也会将款式规定,构建成 CSSOM。浏览器会遍历 CSS 中的规定集,依据 css 选择器创立具备父子,兄弟等关系的节点树。
- 构建 Render 树 。这一步将 DOM 和 CSSOM 关联,确定每个 DOM 元素应该利用什么 CSS 规定。将所有相干款式匹配到 DOM 树中的每个可见节点,并依据 CSS 级联确定每个节点的计算款式,不可见节点(head, 属性包含 display:none 的节点) 不会生成到 Render 树中。
- 布局 / 回流 (Layout/Reflow)。浏览器第一次确定节点的地位以及大小叫布局,如果后续 节点地位以及大小发生变化 ,这一步触发布局调整,也就是 回流。
- 绘制 / 重绘 (Paint/Repaint)。将元素的每个可视局部绘制到屏幕上,包含文本、色彩、边框、暗影和替换的元素(如按钮和图像)。如果 文本、色彩、边框、暗影 等这些元素发生变化时,会触发 重绘 (Repaint)。为了确保重绘的速度比初始绘制的速度更快,屏幕上的绘图通常被分解成数层。将内容晋升到 GPU 层(能够通过 tranform,filter,will-change,opacity 触发) 能够进步绘制以及重绘的性能。
- 合成(Compositing)。这一步将绘制过程中的分层合并,确保它们以正确的程序绘制到屏幕上显示正确的内容。
为啥须要虚构 DOM
下面这是一次 DOM 渲染的过程,如果 dom 更新,那么 dom 须要从新渲染一次,如果存在上面这种状况
<body>
<div id="container">
<div class="content" style="color: red;font-size:16px;">
This is a container
</div>
....
<div class="content" style="color: red;font-size:16px;">
This is a container
</div>
</div>
</body>
<script>
let content = document.getElementsByClassName('content');
for (let i = 0; i < 1000000; i++) {content[i].innerHTML = `This is a content${i}`;
// 触发回流
content[i].style.fontSize = `20px`;
}
</script>
那么须要实在的操作 DOM100w 次, 触发了回流 100w 次。每次 DOM 的更新都会依照流程进行无差别的实在 dom 的更新。所以造成了很大的性能节约。如果循环外面是简单的操作,频繁触发回流与重绘,那么就很容易就影响性能,造成卡顿。另外这里要阐明一下的是,虚构 DOM 并不是意味着比 DOM 就更快,性能须要分场景,虚构 DOM 的性能跟模板大小是正相干。虚构 DOM 的比拟过程是不会辨别数据量大小的,在组件外部只有大量动静节点时,虚构 DOM 仍然是会对整个 vdom 进行遍历,相比间接渲染而言是多了一层操作的。
<div class="list">
<p class="item">item</p>
<p class="item">item</p>
<p class="item">item</p>
<p class="item">{{item}}</p>
<p class="item">item</p>
<p class="item">item</p>
</div>
比方下面这个例子,虚构 DOM。尽管只有一个动静节点,然而虚构 DOM 仍然须要遍历 diff 整个 list 的 class,文本,标签等信息,最初仍然须要进行 DOM 渲染。如果只是 dom 操作,就只有操作一个具体的 DOM 而后进行渲染。虚构 DOM 最外围的价值在于,它能通过 js 形容实在 DOM,表达力更强,通过申明式的语言操作,为开发者提供了更加方便快捷开发体验,而且在没有手动优化,大部分情景下,保障了性能上限,性价比更高。
虚构 DOM
虚构 DOM 实质上是一个 js 对象,通过对象来示意实在的 DOM 构造。tag 用来形容标签,props 用来形容属性,children 用来示意嵌套的层级关系。
const vnode = {
tag: 'div',
props: {id: 'container',},
children: [{
tag: 'div',
props: {class: 'content',},
text: 'This is a container'
}]
}
// 对应的实在 DOM 构造
<div id="container">
<div class="content">
This is a container
</div>
</div>
虚构 DOM 的更新不会立刻操作 DOM,而是会通过 diff 算法,找出须要更新的节点,按需更新,并将更新的内容保留为一个 js 对象,更新实现后再挂载到实在 dom 上,实现实在的 dom 更新。通过虚构 DOM,解决了操作实在 DOM 的三个问题。
- 无差别频繁更新导致 DOM 频繁更新,造成性能问题
- 频繁回流与重绘
- 开发体验
另外因为虚构 DOM 保留的是 js 对象,人造的具备 跨平台 的能力, 而不仅仅局限于浏览器。
长处
总结起来,虚构 DOM 的劣势有以下几点
- 小批改无需频繁更新 DOM,框架的 diff 算法会主动比拟,剖析出须要更新的节点,按需更新
- 更新数据不会造成频繁的回流与重绘
- 表达力更强,数据更新更加不便
- 保留的是 js 对象,具备跨平台能力
有余
虚构 DOM 同样也有毛病,首次渲染大量 DOM 时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。
虚构 DOM 实现原理
次要分三局部
- 通过 js 建设节点形容对象
- diff 算法比拟剖析新旧两个虚构 DOM 差别
- 将差别 patch 到实在 dom 上实现更新
Diff 算法
为了防止不必要的渲染,按需更新,虚构 DOM 会采纳 Diff 算法进行虚构 DOM 节点比拟, 比拟节点差别,从而确定须要更新的节点,再进行渲染。vue 采纳的是 深度优先,同层比拟 的策略。
新节点与旧节点的比拟次要是围绕三件事来达到渲染目标
- 创立新节点
- 删除废节点
- 更新已有节点
如何比拟新旧节点是否统一呢?
function sameVnode(a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 对 input 节点的解决
) || (isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
// 判断两个节点是否是同一种 input 输出类型
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
//input type 雷同或者两个 type 都是 text
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
能够看到,两个节点是否雷同是须要比拟 标签 (tag), 属性 (在 vue 中是用 data 示意 vnode 中的属性 props), 正文节点 (isComment) 的, 另外碰到 input 的话,是会做非凡解决的。
创立新节点
当新节点有的,旧节点没有,这就意味着这是全新的内容节点。只有元素节点,文本节点,正文节点能力被创立插入到 DOM 中。
删除旧节点
当旧节点有,而新节点没有,那就意味着,新节点放弃了旧节点的一部分。删除节点会连带的删除旧节点的子节点。
更新节点
新的节点与旧的的节点都有,那么所有以新的为准,更新旧节点。如何判断是否须要更新节点呢?
- 判断新节点与旧节点是否完全一致, 一样的话就不须要更新
// 判断 vnode 与 oldVnode 是否齐全一样
if (oldVnode === vnode) {return;}
- 判断新节点与旧节点是否是动态节点,key 是否一样,是否是克隆节点 (如果不是克隆节点,那么意味着渲染函数被重置了,这个时候须要从新渲染) 或者是否设置了 once 属性, 满足条件的话替换 componentInstance
// 是否是动态节点,key 是否一样,是否是克隆节点或者是否设置了 once 属性
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
- 判断新节点是否有文本(通过 text 属性判断),如果有文本那么须要比拟同层级旧节点,如果旧节点文本不同于新节点文本,那么采纳新的文本内容。如果新节点没有文本,那么前面须要对子节点的相干状况进行判断
// 判断新节点是否有文本
if (isUndef(vnode.text)) {
// 如果没有文本,解决子节点的相干代码
....
} else if (oldVnode.text !== vnode.text) {
// 新节点文本替换旧节点文本
nodeOps.setTextContent(elm, vnode.text)
}
-
判断新节点与旧节点的子节点相干情况。这里又能分为 4 种状况
- 新节点与旧节点 都有子节点
- 只有新节点有 子节点
- 只有旧节点有 子节点
- 新节点与旧节点 都没有子节点
都有子节点
对于都有子节点的状况,须要对新旧节点做比拟,如果他们不雷同,那么须要进行 diff 操作,在 vue 中这里就是 updateChildren 办法,前面会具体再讲,子节点的比拟次要是双端比拟。
// 判断新节点是否有文本
if (isUndef(vnode.text)) {
// 新旧节点都有子节点状况下,如果新旧子节点不雷同,那么进行子节点的比拟,就是 updateChildren 办法
if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
} else if (oldVnode.text !== vnode.text) {
// 新节点文本替换旧节点文本
nodeOps.setTextContent(elm, vnode.text)
}
只有新节点有子节点
只有新节点有子节点,那么就代表着这是新增的内容,那么就是新增一个子节点到 DOM,新增之前还会做一个反复 key 的检测,并做出揭示,同时还要思考,旧节点如果只是一个文本节点,没有子节点的状况,这种状况下就须要清空旧节点的文本内容。
// 只有新节点有子节点
if (isDef(ch)) {
// 查看反复 key
if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(ch)
}
// 革除旧节点文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 增加新节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
// 查看反复 key
function checkDuplicateKeys(children) {const seenKeys = {}
for (let i = 0; i < children.length; i++) {const vnode = children[i]
// 子节点每一个 Key
const key = vnode.key
if (isDef(key)) {if (seenKeys[key]) {
warn(`Duplicate keys detected: '${key}'. This may cause an update error.`,
vnode.context
)
} else {seenKeys[key] = true
}
}
}
}
只有旧节点有子节点
只有旧节点有,那就阐明,新节点摈弃了旧节点的子节点,所以须要删除旧节点的子节点
if (isDef(oldCh)) {
// 删除旧节点
removeVnodes(oldCh, 0, oldCh.length - 1)
}
都没有子节点
这个时候须要对旧节点文本进行判断,看旧节点是否有文本,如果有就清空
if (isDef(oldVnode.text)) {
// 清空
nodeOps.setTextContent(elm, '')
}
整体的逻辑代码如下
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 判断 vnode 与 oldVnode 是否齐全一样
if (oldVnode === vnode) {return}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 克隆重用节点
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {vnode.isAsyncPlaceholder = true}
return
}
// 是否是动态节点,key 是否一样,是否是克隆节点或者是否设置了 once 属性
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)
}
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)
}
// 判断新节点是否有文本
if (isUndef(vnode.text)) {
// 新旧节点都有子节点状况下,如果新旧子节点不雷同,那么进行子节点的比拟,就是 updateChildren 办法
if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 只有新节点有子节点
if (process.env.NODE_ENV !== 'production') {
// 反复 Key 检测
checkDuplicateKeys(ch)
}
// 革除旧节点文本
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 增加新节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 只有旧节点有子节点,删除旧节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 新旧节点都无子节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新节点文本替换旧节点文本
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
配上流程图会更清晰点
子节点的比拟更新 updateChildren
新旧节点都有子节点的状况下,这个时候是须要调用 updateChildren 办法来比拟更新子节点的。所以在数据上,新旧节点子节点,就保留为了两个数组。
const oldCh = [oldVnode1, oldVnode2,oldVnode3];
const newCh = [newVnode1, newVnode2,newVnode3];
子节点更新采纳的是 双端比拟 的策略,什么是双端比拟呢,就是新旧节点比拟是通过相互比拟首尾元素 (存在 4 种比拟),而后向两头聚拢比拟(newStartIdx, 与 oldStartIdx 递增,newEndIdx 与 oldEndIdx 递加) 的策略。
比拟过程
向两头聚拢
这里对下面呈现的 新前,新后,旧前,旧后 做一下阐明
- 新前,指的是 新节点未解决 的子节点数组中的 第一个 元素,对应到 vue 源码中的newStartVnode
- 新后,指的是 新节点未解决 的子节点数组中的 最初一个 元素, 对应到 vue 源码中的newEndVnode
- 旧前,指的是 旧节点未解决 的子节点数组中的 第一个 元素, 对应到 vue 源码中的oldStartVnode
- 旧后,指的是 旧节点未解决 的子节点数组中的 最初一个 元素, 对应到 vue 源码中的oldEndVnode
子节点比拟过程
接下来对下面的比拟过程以及比拟后做的操作做下阐明
- 新前与旧前 的比拟,如果他们雷同,那么进行下面说到的 patchVnode 更新操作,而后新旧节点各向后一步,进行第二项的比拟 … 直到遇到不同才会换种比拟形式
if (sameVnode(oldStartVnode, newStartVnode)) {
// 更新子节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 新旧各向后一步
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
- 新后与旧后 的比拟,如果他们雷同,同样进行 pathchVnode 更新,而后新旧各向前一步,进行前一项的比拟 … 直到遇到不同,才会换比拟形式
if (sameVnode(oldEndVnode, newEndVnode)) {
// 更新子节点
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 新旧向前
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
- 新后与旧前 的比拟,如果它们雷同,就进行更新操作,而后将旧前挪动到 所有未解决旧节点数组最初面,使旧前与新后地位保持一致,而后单方一起向两头聚拢,新向前,旧向后。如果不同会持续切换比拟形式
if (sameVnode(oldStartVnode, newEndVnode)) {patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 将旧子节点数组第一个子节点挪动插入到最初
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// 旧向后
oldStartVnode = oldCh[++oldStartIdx]
// 新向前
newEndVnode = newCh[--newEndIdx]
- 新前与旧后 的比拟,如果他们雷同,就进行更新,而后将旧后挪动到 所有未解决旧节点数组最后面,是旧后与新前地位保持一致,,而后新向后,旧向前,持续向两头聚拢。持续比拟残余的节点。如果不同,就应用传统的循环遍历查找。
if (sameVnode(oldEndVnode, newStartVnode)) {patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 将旧后挪动插入到最前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 旧向前
oldEndVnode = oldCh[--oldEndIdx]
// 新向后
newStartVnode = newCh[++newStartIdx]
}
- 循环遍历查找,下面四种都没找到的状况下,会通过 key 去查找匹配。
进行到这一步对于没有设置 key 的节点,第一次会通过 createKeyToOldIdx 建设 key 与 index 的映射 {key:index}
// 对于没有设置 key 的节点,第一次会通过 createKeyToOldIdx 建设 key 与 index 的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
而后拿新节点的 key 与旧节点进行比拟,找到 key 值匹配的节点的地位,这里须要留神的是,如果新节点也没 key,那么就会执行 findIdxInOld 办法,从头到尾遍历匹配旧节点
// 通过新节点的 key, 找到新节点在旧节点中所在的地位下标, 如果没有设置 key,会执行遍历操作寻找
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
//findIdxInOld 办法
function findIdxInOld(node, oldCh, start, end) {for (let i = start; i < end; i++) {const c = oldCh[i]
// 找到雷同节点下标
if (isDef(c) && sameVnode(node, c)) return i
}
}
如果通过下面的办法,仍旧没找到新节点与旧节点匹配的下标,那就阐明这个节点是新节点,那就执行新增的操作。
// 如果新节点无奈在旧节点中找到本人的地位下标, 阐明是新元素,执行新增操作
if (isUndef(idxInOld)) {createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
如果找到了,那么阐明在旧节点中找到了 key 值一样,或者节点和 key 都一样的旧节点。如果节点一样,那么在 patchVnode 之后,须要将旧节点 挪动到所有未解决节点之前,对于 key 一样,元素不同的节点,将其认为是新节点,执行新增操作。执行实现后,新节点向后一步。
// 如果新节点无奈在旧节点中找到本人的地位下标, 阐明是新元素,执行新增操作
if (isUndef(idxInOld)) {
// 新增元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 在旧节点中找到了 key 值一样的节点
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 雷同子节点更新操作
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 更新完将旧节点赋值 undefined
oldCh[idxInOld] = undefined
// 将旧节点挪动到所有未解决节点之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果是雷同的 key,不同的元素,当做新节点,执行创立操作
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(oldCh, oldStartIdx, oldEndIdx)
}
子节点比拟总结
下面就是子节点比拟更新的一个残缺过程,这是残缺的逻辑代码
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, newCh, newStartIdx)
// 新旧各向后一步
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 新后与旧后
// 更新子节点
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 新旧各向前一步
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 新后与旧前
// 更新子节点
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 将旧前挪动插入到最初
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// 新向前,旧向后
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 新前与旧后
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 将旧后挪动插入到最前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// 新向后,旧向前
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {// 对于没有设置 key 的节点,第一次会通过 createKeyToOldIdx 建设 key 与 index 的映射 {key:index}
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 通过新节点的 key, 找到新节点在旧节点中所在的地位下标, 如果没有设置 key,会执行遍历操作寻找
idxInOld = isDef(newStartVnode.key) ?
oldKeyToIdx[newStartVnode.key] :
findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果新节点无奈在旧节点中找到本人的地位下标, 阐明是新元素,执行新增操作
if (isUndef(idxInOld)) {
// 新增元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 在旧节点中找到了 key 值一样的节点
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 雷同子节点更新操作
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 更新完将旧节点赋值 undefined
oldCh[idxInOld] = undefined
// 将旧节点挪动到所有未解决节点之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果是雷同的 key,不同的元素,当做新节点,执行创立操作
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(oldCh, oldStartIdx, oldEndIdx)
}
}
参考资料
- VirtualDOM 与 diff
- 渲染页面:浏览器的工作原理
- Vue 中的 DOM-Diff
- 深刻分析:Vue 外围之虚构 DOM