前言
在解读 vue 的 diff 算法时,我将 diff 功能集成到了以前写的 demo 即 vue-mini-demo 项目之中,这个项目已实现的功能还有数据劫持、模版编译和数据响应以及指令绑定(例如,v-for),感兴趣的同学们可以下载下来进行调试。
虚拟 DOM(Virtual DOM)
diff 算法是用来处理虚拟 DOM 的,在了解它之前,我们需要先了解虚拟 DOM。那么什么是虚拟 DOM呢?
一句话概括:虚拟 DOM 就是一个用来描述真实 DOM 的 JavaScript 对象。这样说,可能也就只是让同学们了解到它是个对象。因此,我们需要举个例子,以便让大家能够清楚地理解。
- 真实 DOM
<div>
<span>dom</span>
</div>
- 对应的虚拟 DOM
const Vnode = {
tag: 'ul',
children: [
{ tag: 'span', text: 'dom' }
]
};
通过上述的简易例子,我们不难看出,Virtual DOM 其实就是将真实的 DOM 的数据抽象出来,然后以对象的形式模拟树形结构。
diff 算法
众所周知,渲染真实 DOM 的开销是很大的(尤其是复杂视图的情况下),例如:当我们修改了某个数据,然后直接渲染到真实 DOM 上时,会引起整个 dom 树的重绘和重排。那么有没有办法只更新我们修改的 dom 而不是所有的呢?
答案是:有!那就是 diff 算法。通过它,我们可以将两个虚拟 dom 进行比较,从而找出两者间的差异,之后在利用其它方法(例如,patch 函数)进行 dom 的更新渲染。
下面我们来看一个例子(以 snabbdom 为例):
案例
snabbdom 是一个专注于简单性、模块化、强大功能和性能的虚拟 DOM 库。Vue 2.x 内部使用的虚拟 DOM 就是参考它进行改造的。
网上有很多有关 snabbdom 的使用教程,这里我是直接通过 BootCDN 引入链接的方式使用的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>snabbdom</title>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-attributes.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-dataset.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-props.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-style.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/snabbdom-class.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/snabbdom/0.7.4/h.js"></script>
</head>
<body>
<div id="app"></div>
<button id="btn">新增</button>
<script>
const app = document.getElementById('app')
const { h, init } = window.snabbdom;
// 创建patch函数,用于更新dom
var patch = snabbdom.init([
// 初始化 patch 功能与选择的模块
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
// 第一次渲染
let vnode = h('div#app', [h('p', '1')]);
patch(app, vnode);
// 点击按钮:进行第二次渲染,依据新的虚拟 dom 创建出的 dom 元素将替换之前的,
// 只会修改发生变化的 dom
document.getElementById('btn').addEventListener('click', function () {
let newNode = h('div#app', [h('p', '1'), h('p', '2')]);
patch(vnode, newNode);
});
</script>
</body>
</html>
结果展示
虚拟 dom 的变化,多了一个节点对象。
渲染真实 dom 时的变化,点击新增按钮时,仅渲染了新增的 dom 即前后两个虚拟 dom 的差异之处,其它的都没有变动(有闪动的节点,就是新渲染的)。
通过阅读上面的图文,我们可以知道,第二次 patch 时的 newNode(虚拟 dom) 比第一次 patch 时的 vnode(虚拟 dom),多了一行代码 h('p', '2'),反映到虚拟 dom 中,就是多了一个节点对象。且在渲染时,也仅是对差异之处(新增的节点)进行渲染。
diff 算法比较方式
举 snabbdom 这个例子,主要是想大家进一步了解 diff 算法。下面,我们将开始分析 vue 中的 diff 算法实现。
vue 中的 diff 算法在比较新旧虚拟 dom 时,只会对同层级进行比较, 不会跨层级比较。光说肯定是说不清的,还是要举个例子来看:
- 假如这是我们的真实dom结构
// dom节点1——before
<div>
<p>
<span>1</span>
<span>2</span>
</p>
<p>
<span>3</span>
<span>4</span>
</p>
</div>
// dom节点2——after
<div>
<p>
<span>1</span>
<span>2</span>
</p>
<p>
<span>3</span>
<span>4</span>
</p>
</div>
- 与真实dom相对应的虚拟dom(偷点懒,从网上找了一张别人画好的图)。
将上面的图文对比着来看,想必同学们已了解何为同级比较:就是同一层的 div 与 div 比较或 p 与 p 比较,而不是 div 与 p 或 span 比较。那么,接下来,我们就说一说同级别节点在代码中是如何实现比较的。
具体实现
patch 函数
将 vnode 虚拟节点生成相应 HTML(俗称打补丁),就是从调用此函数开始的。patch 函数接收的oldVnode和vnode 参数分别代表旧节点和新节点,它主要处理以下情况(这里只说核心实现部分):
- oldVnode 旧节点不存在或者是个真实元素(首次渲染时,接受的基本是html),则创建一个新节点。
- 不是真实元素且新旧虚拟节点是同一个对象,则修补更新现有的根节点。
// 为方便阅读与理解,这里仅列出代码的核心功能
function patch(oldVnode, vnode) {
// ...省略
// 老节点不存在
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// 是否为真实元素
const isRealElement = isDef(oldVnode.nodeType);
// 不是真实元素且新旧虚拟节点是同一个对象,则进行修补更新现有的根节点
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode); // 修补现有的根节点
} else {
if (isRealElement) {
// 创建一个空节点替换 oldVnode
oldVnode = emptyNodeAt(oldVnode);
}
// 替换现有的 element
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm); // 获取 oldElm 父元素
// 创建新节点
createElm(
vnode,
insertedVnodeQueue,
parentElm,
// 返回紧跟 oldElm 之后的元素
nodeOps.nextSibling(oldElm)
);
// ...省略
}
}
return vnode.elm;
};
patchVnode 函数
当确定新旧两个虚拟节点需要比较之后,就需调用 patchVnode 函数。它主要负责以下工作:
- 判断 vnode 和 oldVnode 是否为同一个节点对象,如果是,则无需比较,直接 return。
- 若 vnode.text 不存在,分以下情况处理:
- 若 vnode 有子节点而 oldVnode 没有,则将 vnode 的子节点添加到 elm。
- 若 vnode 没有子节点而 oldVnode 有,则删除 elm 的子节点。
- 若两者都有子节点,则执行
updateChildren函数比较并找出这两个子节点的差异,从而进行更新(这是 diff 的核心之处)。
- 若 vnode.text 和 oldVnode.text 存在且不相等,则将 vnode.text 赋值给 elm。
// 为方便阅读与理解,这里仅列出代码的核心功能
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 新旧节点相同即同一个对象,则停止比较
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 克隆重用 vnode。这是 diff 中使用的一种叫就地复用的策略。
// 它指的是尽可能复用之前的 dom,以便在渲染真实 dom 时,减少对 dom 的操作。
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = (vnode.elm = oldVnode.elm);
let i;
const data = vnode.data;
const oldCh = oldVnode.children;
const ch = vnode.children;
// vnode 中的 data 和 tag 属性同时为真,则对其属性(class、style等)进行更新
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) {
cbs.update[i](oldVnode, vnode);
}
}
// vnode.text 为 undefined 或 null
if (isUndef(vnode.text)) {
// oldVnode 和 vnode 都有子节点且不相等,则更新子节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) {
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
} else if (isDef(ch)) { // 只有 vnode 有子节点,则将其子节点添加到elm
checkDuplicateKeys(ch); // 检测 key 是否重复
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ''); // 将elm置空
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) { // 只有 oldVnode 有子节点,则删除其子节点
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, ''); // 将elm置空
}
} else if (oldVnode.text !== vnode.text) {
// 若是 vnode 和 oldVnode 都有属性 text(文本节点)并且不相等,则将 vnode.text 赋值给elm
nodeOps.setTextContent(elm, vnode.text);
}
}
updateChildren 函数
updateChildren 函数,可以说是 diff 的核心,它主要是比较并找出新旧虚拟节点的子节点差异,以便实现最小化更新。
新旧子节点比较概括
updateChildren 函数接收的 oldCh 和 newCh 参数分别代表旧子节点和新子节点,它们各有两个头尾节点和两个头尾节点索引:
-
节点
- oldStartVnode —— 旧子节点的开始节点
- oldEndVnode —— 旧子节点的结束节点
- newStartVnode —— 新子节点的开始节点
- newEndVnode —— 新子节点的结束节点
-
节点索引
- oldStartIdx —— 旧子节点的开始节点索引
- oldEndIdx —— 旧子节点的结束节点索引
- newStartIdx —— 新子节点的开始节点索引
- newEndIdx —— 新子节点的结束节点索引
这些变量组合成四种比较方式。假若这四种方式都没匹配到,但节点设置了 key,那就会用 key 进行比较。而在比较的过程中,这些变量会逐渐向中间移动。当 oldStartIdx > oldEndIdx 或 newStartIdx > newEndIdx,则表明 oldCh 和 newCh 至少有一个已完成遍历,就会结束比较。
新旧子节点比较方式
- oldStartVnode 和 newStartVnode 比较。
- oldEndVnode 和 newEndVnode 比较。
- oldStartVnode 和 newEndVnode 比较。
- oldEndVnode 和 newStartVnode 比较。
- 以上4种比较都没匹配到,若是设置了
key,就用key进行比较。
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 是仅使用 <transition-group> 时的一个特殊标志,
// 以确保在离开转换期间被移除的元素保持在正确的相对位置
const canMove = !removeOnly;
checkDuplicateKeys(newCh); // 检查 key 值是否重复
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// oldStartVnode 向右移动
oldStartVnode = oldCh[++oldStartIdx];
} else if (isUndef(oldEndVnode)) {
// oldEndVnode 向左移动
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// oldStartVnode 和 newStartVnode 是同一个节点,则进行比较
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
// oldStartVnode 和 newStartVnode 向右移动
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// oldEndVnode 和 newEndVnode 是同一个节点,则进行比较
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
// oldEndVnode 和 newEndVnode 向左移动
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// oldStartVnode 和 newEndVnode 是同一个节点,则进行比较
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
canMove &&
// 插入元素
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
);
// oldStartVnode 向右移动,newEndVnode 向左移动
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// oldEndVnode 和 newStartVnode 是同一个节点,则进行比较
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
canMove &&
// 插入元素
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
// oldEndVnode 向左移动,newStartVnode 向右移动
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else { // 第五种比较: 通过 key 来进行比较
if (isUndef(oldKeyToIdx)) {
// 创建一个以旧子节点的 key 和索引做为键与值的对象并返回
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 获取当前的旧子节点索引
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
// 找出 oldCh(旧节点数组)中和 newStartVnode 是同一个节点的元素,然后返回其索引
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 判断 idxInOld 是否存在
if (isUndef(idxInOld)) {
// 创建新节点
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
} else {
// 取出当前需要比较的旧子节点
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 向右移动
newStartVnode = newCh[++newStartIdx];
}
}
// 退出判断循环后,说明新或旧节点数组有一个已被查找完,这时有两种情况要处理:
// 第一种: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(oldCh, oldStartIdx, oldEndIdx);
}
}
图文解析
为了更好的理解 updateChildren 的代码逻辑(diff 过程),下面我将用图文结合的方式对它进行一次解析。
第一种比较方式
oldStartVnode 和 newStartVnode 进行比较:
-
执行
sameVnode判断是否为相同节点。 -
若是相同节点,则执行
patchVnode找出两者间的差异,有差异就更新视图,没有差异则什么都不做。在本例的真实 dom 中,文本内容 A 会更新为 a。 -
oldStartVnode 和 newStartVnode 向右移动。
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
第二种比较方式
oldEndVnode 和 newEndVnode 进行比较:
-
执行
sameVnode判断是否为相同节点。 -
若是相同节点,则执行
patchVnode找出两者间的差异,有差异就更新视图,没有差异则什么都不做。在本例的真实 dom 中,文本内容 D 会更新为 d。 -
oldEndVnode 和 newEndVnode 向左移动
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
第三种比较方式
oldStartVnode 和 newEndVnode 比较:
-
执行
sameVnode判断是否为相同节点。 -
若是相同节点,则执行
patchVnode找出两者间的差异,有差异就更新视图,没有差异则什么都不做。在本例的真实 dom 中,文本内容 A 会更新为 d。 -
oldStartVnode 对应的真实dom 位移到 oldEndVnode 对应的真实 dom 之后。
// 插入元素
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
);
- oldStartVnode 向右移动,newEndVnode 向左移动。
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
第四种比较方式
oldEndVnode 和 newStartVnode 比较:
-
执行
sameVnode判断是否为相同节点。 -
若是相同节点,则执行
patchVnode找出两者间的差异,有差异就更新视图,没有差异则什么都不做。在本例的真实 dom 中,文本内容 D 会更新为 a。 -
oldEndVnode 对应的真实 dom 位移到 oldStartVnode 对应的真实 dom 之前。
// 插入元素
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
- oldEndVnode 向左移动,newStartVnode 向右移动。
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
第五种比较方式
若是以上4种比较都没匹配到,但设置了key,那就用key进行比较。下面有关大概思路和处理过程的叙述,建议结合 updateChildren 中关于用 key 进行比较的代码来看。
大概思路:
先通过 key 找出 oldCh(旧子节点数组)中与 newStartVnode(新子节点的开始节点)相同的节点的索引。若是没找到,就让 newStartVnode 同 oldCh 中的节点逐个比较,找出相同节点的索引。最后,根据这个索引是否存在,判断是进行节点比较还是创建新节点。
处理过程:
- 索引(idxInOld)不存在,执行
createElm创建新节点。 - 索引(idxInOld)存在,则根据索引从 oldCh 中取出这个节点(vnodeToMove)进行比较。
- 执行
sameVnode判断是否为相同节点。 - 是相同节点,则执行
patchVnode找出两者间的差异,有差异就更新视图,没有差异则什么都不做。然后,将此节点对应的真实 dom 移动到 oldStartVnode 对应的真实 dom 之前。 - 不是相同节点(相同的 key,但却是不同的元素,则视为新元素),执行
createElm创建新节点。
- 执行
- newStartVnode 向右移动
上述处理过程,在整体上分为两种情况:有相同节点和没有相同节点(需调用 createElm 函数)。 注意,实线尖头代表找到了相同节点,虚线尖头代表没找到相同节点。
- 有相同节点的示意图
- 没有相同节点的示意图
退出循环后的处理
代码展示:
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);
}
退出循环,意味着在新子节点数组和旧子节点数组中,至少有一个已被查找完。从代码可以看出,这分为两种处理情况:
oldStartIdx > oldEndIdx(旧子节点数组先遍历完),则说明 newCh(新子节点数组)中的节点比 oldCh(旧子节点数组)中的节点要多,也就是 newCh 中有剩余节点,这些节点需要新增。总结成一句话就是:oldCh 中没有的子节点,而 newCh 中有,就新增。
新增节点是通过调用 addVnodes 函数。但实际上,它在执行时会去调用 createElm 函数创建节点元素,然后通过 insert 函数,将节点元素插入到相应位置。
// 为便于了解代码逻辑,所以省略了一些代码
function createElm(
vnode, // 虚拟节点对象
insertedVnodeQueue, // 存储已插入的 vnode 的队列
parentElm, // vnode.elm 父元素
refElm, // 紧跟在 vnode.elm 之后的元素
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 这个 vnode 在之前的渲染中使用过!
// 现在它被用作一个新节点,当它被用作插入参考节点时,覆盖它的 elm 会导致潜在的补丁错误。
// 相反,我们在为节点创建相关的 DOM 元素之前按需克隆节点。
vnode = ownerArray[index] = cloneVNode(vnode);
}
const data = vnode.data; // 获取元素属性
const children = vnode.children; // 获取子元素
const tag = vnode.tag; // 获取标签
// 元素节点
if (isDef(tag)) {
// 创建元素
vnode.elm = nodeOps.createElement(tag, vnode);
// 创建子元素
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
// 处理元素上的各种属性
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// 插入元素
insert(parentElm, vnode.elm, refElm);
} else {
// 纯文本节点
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
// 为便于了解代码逻辑,所以省略了一些代码
function insert(parent, elm, ref) {
// 存在父级
if (isDef(parent)) {
// elm 之后存在元素(有同级的兄弟元素)
if (isDef(ref)) {
// elm 和 ref 元素的父级元素是同一个(elm 和 ref是同级兄弟元素)
if (nodeOps.parentNode(ref) === parent) {
// 将 elm 插入到 ref 之前
nodeOps.insertBefore(parent, elm, ref);
}
} else {
// elm 之后不存在元素,也就是 ref 不存在,则将 elm 插入到 parent 节点
// 元素列表(childNodes[] 数组)的末尾
nodeOps.appendChild(parent, elm);
}
}
}
新增的元素节点的位置示意图:
- 新增的元素节点后不存在元素节点
- 新增的元素节点后存在元素节点
newStartIdx > newEndIdx(新子节点数组先遍历完),则说明 oldCh(旧子节点数组)中的节点比 newCh(新子节点数组)中的节点要多,也就是 oldCh 中有剩余节点,这些节点需要删除。总结成一句话就是:newCh 中没有的子节点,而 oldCh 中有,就删除。
删除节点主要通过调用 removeVnodes 函数。
function removeVnodes(vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (isDef(ch)) { // ch 是否存在
removeNode(ch.elm);
}
}
}
function removeNode(el) {
const parent = nodeOps.parentNode(el);
if (isDef(parent)) { // parent 是否存在
nodeOps.removeChild(parent, el); // 删除
}
}
删除元素节点的示意图:
结束
在阅读的过程中,如果同学们发现了说的不对的地方,还请不吝赐教。当然,若是你觉得有所收获,还请为我点个赞👍,谢谢!