引言
在如今的面试中,对候选人源码原理层面的考察,越来越重视。其中 diff 算法,问的尤为高频。在拜读了多位大佬的分析文章并对照源码,写一些自己对 diff 的理解,希望对您有所帮助。
VirtualDOM
个人的理解,virtual dom就是真实dom的一种抽象,是 JavaScript 对真实dom的描述。现在主流前端框架几乎都有 virtual dom这个概念的原因,无非有以下这几点。
- 可以通过diff算法,找出最小差异,然后进行更新操作,更有效率。
- 应用跨平台成为需求,通过vdom 抽象真实 dom,更加适合于跨平台,只需在不同平台,将vdom生成相应真实dom即可。
- 为前后端同构提供了可能性。
diff 的方式
在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。

何时调用
当改变响应式属性时,会调用属性的 set 方法, 进而调用 dep.notify(); 依赖于此响应式属性的Watcher就会通过patch比对oldVnode、vnode,得出最小差异,更新视图。
patch
// src/core/vdom/patch.js
// 代码均为删除了非主要代码,保留重要流程逻辑的代码
// 英文注释,为vue2.6 源码自带, 中文注释为本人添加
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 比较是否是 sameVnode
// patch existing root node
patchVnode(oldVnode, vnode);
} else {
// 如果不是 sameVnode 直接获取 oldVnode 的 parent,删除 oldVnode,再添加 vnode
// replacing existing element
const oldElm = oldVnode.elm; // Vnode 中elm属性保存真实的 element
const parentElm = nodeOps.parentNode(oldElm); // 获取 oldVnode 的父节点
createElm(vnode);
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
}
}
return vnode.elm;
};
sameVnode
// 比较 oldVnode vnode 的 key, tag, isComment, data是否相同
// 如果是input标签,还需比较 type 是否相同
// 因为有些浏览器不支持动态修改 input 输入框的 type 值
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
)
)
}
patchVnode
function patchVnode(oldVnode, vnode) {
if (oldVnode === vnode) {
return; // 相等直接返回
}
const elm = (vnode.elm = oldVnode.elm);
// reuse element for static trees.
// note we only do this if the vnode is cloned -
// if the new node is not cloned it means the render functions have been
// reset by the hot-reload-api and we need to do a proper re-render.
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 oldCh = oldVnode.children;
const ch = vnode.children;
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
updateChildren(elm, oldCh, ch);
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch); // 此处可说明,key 不可重复
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '');
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1); // 如果oldCh 存在, ch不存在,则清空 oldCh
} else if (isDef(oldVnode.text)) {
// vnode 无 文本节点, oldVnode 有文本节点,则清空oldVnode 文本节点
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
}
- 如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。
- 新老节点均有children节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。
- 如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
- 当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
- 当新老节点都无子节点的时候,只是文本的替换。
updateChildren
千呼万唤始出来,兜兜转转许久,终于见到了 updateChildren 函数,其就是 diff 的核心了。
function updateChildren (parentElm, oldCh, newCh) {
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
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)) { // 新老vnode 互相比较首位两个节点,总共四种方式
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)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果首尾两两都不满足 sameVnode
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 将oldVnode 的 key 生成一张hash map
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element 不存在,则新建element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {// 在oldVnode 中有相同 key 的节点
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
// 比较是否是 sameVnode 如果是,就可以复用。否则不行
// key 的作用是增加节点的复用性,而不只单单比较首尾两个节点。避免新建节点,增加开销。
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) { // 说明 oldVnode 比 vnode少, 则新建vnode剩下的节点,添加进parentElm
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) { 说明 oldVnode 比 vnode多,清空剩余所有oldVnode
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
createKeyToOldIdx
// 将children的key 生成一张hash表
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
总结
以上就是个人对 diff 的理解,为便于阅读,代码有所删减。尽量抓大放小,抓住主要流程,理清执行过程,有助于读懂源码。希望能对大家有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。