1、最近好几个月都比较忙,刚好项目接近尾声,今天终于抽出来了一点时间重新捋了一下diff算法,具体的解析后面抽时间补上,画了个大概的图,可以配合代码注释凑活着看
2、草稿箱里也放了好多篇想写的文章,后面整理成一个系列慢慢发出来
这个问题困扰我挺久的,在网上搜过大佬们谈怎么阅读源码的心得,总觉得拿来自己套用起来并不适用,这里分享下自己阅读源码时的思路
我读 diff 的原因更多的在于好奇,另外之前在掘金看 v-for为什么不推荐使用index作为key 这篇文章时,看的有点云里雾里 ,懂了但又没懂。另外这些涉及到diff 的文章,都会多次提及 dom复用。
了解过的同学可以直接跳过这一段
前端现在最火的三大框架angular、vue、react都做到了响应式,即数据更新之后,视图会响应数据变更从而重新渲染页面。关于响应式原理这里不做赘述,那么视图重新渲染这一步骤,三大框架是怎么做的呢?
vue 和 react 都使用了 虚拟dom ,配合 diff算法 进行 dom 节点的移动、添加、修改、删除。
而 angular 我扒了很久都没找到对应的文章,但是ng里好像并没有虚拟dom的概念,后面研究后再补上。
所谓虚拟dom就是 用js代码描述一段html代码,简单举个栗子
<li key="li">
Virtual dom
<li>
转换为虚拟dom
{
tag: 'li',
text: 'Virtual dom',
key: 'li',
...
}
vue 通过 createElement 方法创建 虚拟dom,该方法具体做了什么有兴趣的可以自己去github研究下,代码路径如下图
下面是vue对于虚拟dom的定义,可以简单看一下
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
关于virtual dom的优缺点可以参考这篇 掘金文章
首先需要明确传入的参数都代表什么:
oldVnode 为当前dom元素映射成的虚拟dom节点
vnode 为数据更新后,通过更新后的数据生成的虚拟dom节点
insertedVnodeQueue 为节点插入队列,节点的插入操作都放在该队列中,而这个队列真正使用的地方,是在patch函数的最后,调用了invokeInserthook,消费这个队列内所有的操作,至于invokeInserthook函数的作用,大胆的猜测一下,就是最终执行dom节点的插入操作呗。
/**
*
* @param oldVnode 旧的虚拟dom节点
* @param vnode 新的虚拟dom节点
* @param insertedVnodeQueue 节点插入队列
* @param ownerArray 在patch中调用时为null, updateChildren中为新节点的children
* @param index 在patch中调用时为null
* @param removeOnly removeOnly 是一个只有 使用的特殊标志,确保在离开过渡期间移除的元素保持在正确的相对位置
* @returns
*/
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
1、先比较新旧节点是否指向同一个引用,如果是相同的,也就不需要更新了,直接return就好
if (oldVnode === vnode) {
return;
}
2、
思维导图地址:https://www.processon.com/view/link/6169523fe0b34d7c7db4a697
/**
*
* @param oldVnode 旧的虚拟dom节点
* @param vnode 新的虚拟dom节点
* @param insertedVnodeQueue 节点更新队列(队列中的操作都是异步执行,执行时机一般为当前执行周期的最后)
* @param ownerArray 在patch中调用时为null, updateChildren中为新节点的children
* @param index 在patch中调用时为null
* @param removeOnly removeOnly 是一个只有 使用的特殊标志,确保在离开过渡期间移除的元素保持在正确的相对位置
* @returns
*/
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// 克隆重用的vnode
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;
}
// 为静态树重用元素,仅在克隆 vnode 时执行此操作 -
// 如果新节点没有被克隆,则意味着渲染函数已经由 hot-reload-api 重置,我们需要进行适当的重新渲染。
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)) {
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)) {
// 如果新旧节点都存在子节点
if (isDef(oldCh) && isDef(ch)) {
// 且新旧子节点并不指向同一个,则执行updateChildren
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, '');
// 然后直接把新节点的子节点添加到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);
}
}
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
// 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];
// 比较新旧首节点,如果类似就把进行patchVnode操作(加入到节点更新队列)
} 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)) {
// Vnode moved right
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)) {
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 如果还没有构建map
if (isUndef(oldKeyToIdx))
// 以旧节点的key为key,指针为value构建map
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
// 如果新节点存在key,就在这个map中匹配新节点的key,idxInOld为匹配到的旧节点的索引
// 如果不存在就循环所有的旧节点,比较新节点是否存在与旧节点中的某个节点相似,匹配上就返回该旧节点的索引
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// 如果匹配不到就直接创建新节点,插入到旧首节点之前
if (isUndef(idxInOld)) {
// New element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
// key相同或找到类似的节点
} else {
vnodeToMove = oldCh[idxInOld];
// 如果是相似节点,就更新该节点,并且在旧节点中删除该节点,把更新后的节点移动旧首节点前
// 这里多判断了一遍,过滤掉key相同而元素不同的节点
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldCh[idxInOld] = undefined;
canMove &&
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// 否则即使是相同的key,但是由于元素不同,依旧会创建新节点
// same key but different element. treat as new element
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);
}
}