携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第21天,点击查看活动详情
前言
Vue的视图更新是通过新旧虚拟DOM对比差异,找出需要更新的地方,操作最少真实DOM来实现更新视图。对比 VNode 并找出差异的过程被称为 DOM-Diff 过程。
patch
Vue中, DOM-Diff 过程也叫做 patch 过程。 patch 是“补丁”的意思,以数据发生变化后生成新的 VNode 为标准,对比之前旧的 VNode,如果新的 VNode 上有的节点而旧的 VNode 上没有,就在旧的 VNode 上加上,反之亦然,从而达到来两个 VNode相同。
其实整个过程主要就做了三件事:
- 创建节点:新的
VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。 - 删除节点:新的
VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。 - 更新节点:新的
VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。
创建节点
VNode中可以描述6种类型的节点,但只有元素节点、文本节点、注释节点可以被创建插入到 DOM 中。所以在创建节点的时候会先判断是什么节点类型,在调用对应的方法创建并插入到 DOM 中。
// 源码位置: /src/core/vdom/patch.js
function createElm(vnode, parentElm, refElm) {
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); // 创建元素节点的子节点
insert(parentElm, vnode.elm, refElm); // 插入到DOM中
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text); // 创建注释节点
insert(parentElm, vnode.elm, refElm); // 插入到DOM中
} else {
vnode.elm = nodeOps.createTextNode(vnode.text); // 创建文本节点
insert(parentElm, vnode.elm, refElm); // 插入到DOM中
}
}
- 判断该
VNode节点是否有tag属性,如果有就认为是元素节点,调用createElement方法创建元素节点,如果元素节点还有子节点,那就递归遍历创建所有子节点,把所有子节点insert到当前元素节点后,再把当前元素节点insert到DOM中。 - 判断
VNode的isComment属性,如果为true则是注释节点,调用createComment方法创建注释节点,再插入到DOM中。 - 如果两个都不满足,则认为是文本节点,调用
createTextNode方法创建文本节点,再插入到DOM中。
删除节点
新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可。
function removeNode(el) {
const parent = nodeOps.parentNode(el); // 获取父节点
if (isDef(parent)) {
nodeOps.removeChild(parent, el); // 调用父节点的removeChild方法
}
}
更新节点
在更新节点中主要分为三种情况判断并分别处理
静态节点
<p>我是不会变化的文字</p>
在这个节点中只包含了纯文字,没有变量,任何数据发生变化都和它无关,称这种节点为静态节点。
如果 VNode 和 oldVNOde 均为静态节点,则直接跳过,无需处理。
文本节点
如果 VNode 是文本节点,只需看 oldVNode 是否也是文本节点,如果是,则比较内容是否相同,不同则把 oldVNode 里的文本改成跟 VNode 的一样。如果 oldVNode不是文本节点,直接调用 setTextNode方法把它改成文本节点,并且内容和 VNode 相同。
元素节点
如果是元素节点,又被细分为两个情况:
-
该节点包含子节点
如果新的节点内包含子节点,如果旧的节点也包含子节点,就递归对比更新子节点;如果旧的节点不包含子节点,那么可能是空节点或者文本节点,如果是空节点,则把新的节点里的子节点创建一份并插入到旧的节点里面,如果是文本节点,则把文本清空,再执行空节点的操作。
-
该节点不包含子节点
如果该节点不包含子节点,同时它又不是文本节点,则说明它是个空节点,直接把旧的节点清空即可。
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// vnode与oldVnode是否完全一样?若是,退出程序
if (oldVnode === vnode) {
return;
}
const elm = (vnode.elm = oldVnode.elm);
// vnode与oldVnode是否都是静态节点?若是,退出程序
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
return;
}
const oldCh = oldVnode.children;
const ch = vnode.children;
// vnode有text属性?若没有:
if (isUndef(vnode.text)) {
// vnode的子节点与oldVnode的子节点是否都存在?
if (isDef(oldCh) && isDef(ch)) {
// 若都存在,判断子节点是否相同,不同则更新子节点
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
// 若只有vnode的子节点存在
else if (isDef(ch)) {
/**
* 判断oldVnode是否有文本?
* 若没有,则把vnode的子节点添加到真实DOM中
* 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
}
// 若只有oldnode的子节点存在
else if (isDef(oldCh)) {
// 清空DOM中的子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 若vnode和oldnode都没有子节点,但是oldnode中有文本
else if (isDef(oldVnode.text)) {
// 清空oldnode文本
nodeOps.setTextContent(elm, "");
}
// 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
}
// 若有,vnode的text属性与oldVnode的text属性是否相同?
else if (oldVnode.text !== vnode.text) {
// 若不相同:则用vnode的text替换真实DOM的文本
nodeOps.setTextContent(elm, vnode.text);
}
}
小结
- 分析
patch过程,理解算法思想,知道整个过程就是做三件事:创建节点、删除节点、更新节点。 - 针对每个过程对照源码理解其逻辑,看懂流程图。
- 如果新旧节点都有子节点,那就需要更新子节点,那么子节点是怎么更新的呢?