1、最近好几个月都比较忙,刚好项目接近尾声,今天终于抽出来了一点时间重新捋了一下diff算法,具体的解析后面抽时间补上,画了个大概的图,可以配合代码注释凑活着看 2、草稿箱里也放了好多篇想写的文章,后面整理成一个系列慢慢发出来
@TOC
1、怎么更好的阅读源码
这个问题困扰我挺久的,在网上搜过大佬们谈怎么阅读源码的心得,总觉得拿来自己套用起来并不适用,这里分享下自己阅读源码时的思路
- 明确自己的主线任务,即自己读这个源码是想干嘛,写代码过程中遇到问题,需要解惑?还是想了解这个技术的实现原理,学习作者的设计思路。
- 如果是需要解惑,可以对心里的疑惑先做个推测,大胆的猜一下问题点,然后带着问题去读。
- 如果是单纯的想要学习,提升自己,推荐先去扒几篇高质量的博客(高赞文章),大致了解下这个技术出现的背景,解决了什么,有什么闪光点,然后可以稍微记下博客中高频出现的关键字,阅读源码的过程可以重点关注下这些闪光点,关键字。
- 不管出于哪种目的,在分析源码的过程中遇到看不懂的代码块,可以直接暂时跳过,没必要死磕,看不懂可太正常了。如果能感觉到它跟主线任务不搭边,那就直接跳。
- 读完第一遍之后,心里基本对逻辑有个大概了解了,推荐用画图软件,从头回忆一下刚刚的代码逻辑,画一个思维导图出来,这样可以帮自己理清思路,把知识点串起来,遇到模糊不清的点,也可以倒回去针对性的研究。
- 源码中经常会调用一些其他文件中的方法,这时候可以根据方法命名去猜一下方法的作用,优秀的源码,作者的命名通常都会很规范。但有些重要的逻辑,可能还是需要跳转到方法的实现大致分析一下。
我读 diff 的原因更多的在于好奇,另外之前在掘金看 v-for为什么不推荐使用index作为key 这篇文章时,看的有点云里雾里 ,懂了但又没懂。另外这些涉及到diff 的文章,都会多次提及 dom复用。
2、虚拟dom (virtual dom)
了解过的同学可以直接跳过这一段
前端现在最火的三大框架angular、vue、react都做到了响应式,即数据更新之后,视图会响应数据变更从而重新渲染页面。关于响应式原理这里不做赘述,那么视图重新渲染这一步骤,三大框架是怎么做的呢?
vue 和 react 都使用了 虚拟dom ,配合 diff算法 进行 dom 节点的移动、添加、修改、删除。 而 angular 我扒了很久都没找到对应的文章,但是ng里好像并没有虚拟dom的概念,后面研究后再补上。
所谓虚拟dom就是 用js代码描述一段html代码,简单举个栗子
html
<li key="li">
Virtual dom
<li>
转换为虚拟dom
javascript
{
tag: 'li',
text: 'Virtual dom',
key: 'li',
...
}
vue 通过 createElement 方法创建 虚拟dom,该方法具体做了什么有兴趣的可以自己去github研究下,代码路径如下图 下面是vue对于虚拟dom的定义,可以简单看一下
typescript
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的优缺点可以参考这篇 掘金文章
3、patchVnode 更新dom节点
patchVnode的调用入口,在patch函数中
javascript
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 如果新的虚拟dom节点不存在,说明旧节点已被删除,直接销毁旧dom节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 如果旧节点不存在,说明是新增,直接创建新的dom
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由此处调用
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 安装到真实元素,检查是否是服务器渲染的内容以及是否可以执行
... 此处省略一部分逻辑处理,与主线无关
}
// 直接创建一个新的dom元素,替换旧的dom
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
这里要注意一点,符合sameVnode条件才会执行patchVnode 不符合条件的,会直接创建新的dom元素去替换掉旧的,注意看上面的代码走向。
下面是patchVnode的具体实现
javascript
/**
*
* @param oldVnode 旧的虚拟dom节点
* @param vnode 新的虚拟dom节点
* @param insertedVnodeQueue 节点更新队列(队列中的操作都是异步执行,执行时机一般为当前执行周期的最后)
* @param ownerArray 在patch中调用时为null, updateChildren中为新节点的children
* @param index 在patch中调用时为null
* @param removeOnly removeOnly 是一个只有 <transition-group> 使用的特殊标志,确保在离开过渡期间移除的元素保持在正确的相对位置
* @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);
}
}
首先需要明确传入的参数都代表什么:
- oldVnode 为当前dom元素映射成的虚拟dom节点
- vnode 为数据更新后,通过更新后的数据生成的虚拟dom节点
- insertedVnodeQueue 为节点插入队列,节点的插入操作都放在该队列中,而这个队列真正使用的地方,是在patch函数的最后,调用了invokeInserthook,消费这个队列内所有的操作,至于invokeInserthook函数的作用,大胆的猜测一下,就是最终执行dom节点的插入操作呗。
javascript
/**
*
* @param oldVnode 旧的虚拟dom节点
* @param vnode 新的虚拟dom节点
* @param insertedVnodeQueue 节点插入队列
* @param ownerArray 在patch中调用时为null, updateChildren时为新节点的children
* @param index 在patch中调用时为null
* @param removeOnly removeOnly 是一个只有 <transition-group> 使用的特殊标志,确保在离开过渡期间移除的元素保持在正确的相对位置
* @returns
*/
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
下面来看具体的代码逻辑:
1、先比较新旧节点是否指向同一个引用,如果是相同的,也就是说不需要更新,直接return,本次比较结束
javascript
if (oldVnode === vnode) {
return;
}
2、后面这一大坨我表示基本看不懂,但是跟我的主线任务并不相关,直接跳过 3、后面这段逻辑是patchVnode方法的核心点
isUndef 判断当前传入的参数是不是 undefined,是则返回true isDef 判断当前传入的参数是不是undefined,不是则返回true
ps: 这里本来想用文字写出各种判断的,但是写完发现,还没有思维导图来的清晰
4、updateChildren
在patchVnode的逻辑中,当新旧节点都存在子节点,且子节点不同时,会调用updateChildren,进行子节点的更新。
javascript
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];
// 比较新旧首节点,如果类似就把进行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);
}
}
参数
javascript
/**
* @param {*} parentElm 父元素 (父元素真实的elm)
* @param {*} oldCh 旧的子节点数组
* @param {*} newCh 新的子节点数组
* @param {*} insertedVnodeQueue 队列,同patchVnode
* @param {*} removeOnly 同patchVnode
*/
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
)
具体的代码逻辑:
1、声明变量 updateChildren方法开始有一大堆定义变量的代码,但实际上这里的代码命名都非常规范,看名字基本就能看出是什么意思
javascript
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];
至于下面这段,后面具体代码逻辑中,用到的时候再解释
javascript
let oldKeyToIdx, idxInOld, vnodeToMove, refElm;
1.1 某支线逻辑
还记得当我们使用v-for时,不小心使用了重复的key,控制台中抛出的错吗? 2、比较并更新子节点 在了解这部分代码之前,先介绍下sameVnode方法 sameVnode 用于比较节点是否是相似节点,是相似节点,才会进行patchVnode操作,给节点打更新补丁。 该方法主要比较 tag 和 key 是否一致,有兴趣的可以去看下源码
在while循环中,xxxStartIdx 和 xxxEndIdx 都会不停的移动,xxxStartIdx 从 0 向 数组尾部移动,而 xxxEndIdx 从数组尾部,向头部移动。对应的xxxStartVnode 和 xxxEndVnode也会随着idx变化而变化
javascript
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果旧的起始节点不存在,这里我也不是很懂,为什么会存在旧节点数组会存在节点为空的情况,不知道在生成子虚拟节点数组时,做了什么处理
if (isUndef(oldStartVnode)) {
// 如果不存在则移动指针,更新 oldStartVnode,此时oldStartVnode变成了数组的第二个元素,oldStartIdx也变成了 1
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];
}
}
这部分比较有意思的是比较的设计思路:
- 先比较新旧节点的首节点是否一致,再比较尾节点是否一致,如果一致,直接进行patchVnode操作
- 再比较旧首和新尾,旧尾和新首,判断节点是否直接调换了首尾位置,如果调换了位置,则在更新节点后把节点到对应正确的位置
上述的四步比较用于检查首、尾节点是否发生了偏移。 当上面的四种情况都匹配不上的时候,检查旧节点数组中有没有与该节点构成sameVnhode关系的,有就更新该节点,并且在旧节点中删除该节点,把更新后的节点移动到旧首节点。
查找方式如下:
-
以旧节点的key为key,索引(index)为value构建一个对象 举例: 旧节点 A,B,C 假如节点的key依次为A,B,C 生成的对象为 {A:0, B:1, C:2}
-
如果新节点存在key,就在上面构成的对象中匹配新节点的key,匹配到就返回value(即匹配到的旧节点在旧节点数组中对应的索引)
-
如果新节点不存在key,就循环所有的旧节点,比较新节点是否存在与旧节点中的某个节点相似,匹配上就返回该旧节点的索引
这步判断补上了剩余的所有发生偏移的可能性 在理解这部分逻辑的时候,要明确新旧首尾节点,是随着比较不停变化的,首节点不断向后移动,尾结点不断向前移动,指针Idx在不断向内收缩,直到不符合while条件循环结束。
3、当循环结束时,处理剩余未对比的节点 从while的条件可以看出,循环结束时有两种情况,要么是旧子节点先循环完,要么新子节点先循环完。
javascript
// 如果因为旧的开始索引大于旧结束索引而结束的循环,则说明还存在新的节点没有对比,直接把这些节点依次加上
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);
}
最后的最后,我们从patchVnode开始,来一起理一下更新过程:
- 首先我们更新了数据,触发了vue的视图更新机制,用新的数据生成了新的虚拟dom,然后获取旧数据对应的虚拟dom,调用patch进行对比,patch的逻辑内,相似的节点,调用patchVnode进行更新。
- 然后在patchVnode里 2.1 先判断新旧节点是否存在文本信息,存在则对比文本是否相等,不相等使用新节点的文本覆盖旧节点,相等则不做任何操作。 2.2 不存在文本时,分支逻辑较多。 ① 新旧节点存在子节点,旧不存在时执行以下步骤 :step1 如果不是生产环境,就检测key是否重复,重复则在控制台中抛错。step2 旧节点如果存在文本则先把文本置空 step3 把新节点的子节点添加页面元素中。 ② 旧节点中存在而新节点不存在时,直接删除该页面元素中的子节点。 ③ 旧节点中存在文本,则先把文本置空
- 在updateChildren中,也进行了一连串的逻辑判断,判断子节点的区别,并对不同的逻辑分支做不同的处理。其中部分情况下,比如节点符合sameVnode,会调用patchVnode方法,更新节点信息(子节点的子节点也有可能会触发updateChildren,然后也有可能再触发updateChildren),所以比较会一直进行到节点的最深层。
5、思维导图和整体的代码注释
思维导图地址:www.processon.com/view/link/6...](p3-juejin.byteimg.com/tos-cn-i-k3...)