vue源码版本为2.6.11(cdn地址为: https://lib.baomitu.com/vue/2.6.11/vue.js)
虚拟DOM最核心的部分是patch,它可以将vnode渲染成真实的DOM。通过比对新旧两个vnode,找出需要更新的节点进行更新。
更新的操作包括:
- 创建新增的节点
- 删除已经废弃的节点
- 修改需要更新的节点
新增节点
什么情况下创建元素并将元素渲染到视图?
1. 首次渲染时,DOM中不存在任何节点,oldVnode并不存在,使用vnode创建元素并渲染视图;
如模板:
<div id="app">
<h1>{{message}}</h1>
</div>
首次渲染时,先创建元素并插入body里,并移除之前的模板
2. 当oldVnode与vnode完全不是同一个节点时,是使用vnode创建一个新DOM节点,用它去替换oldVnode所对应的真实DOM节点
创建节点
只有三种类型的节点会被创建并插入到DOM中:元素节点、注释节点、文本节点。
元素节点
判断vnode是否是元素节点,只需要判断它是否具有tag属性即可;是元素节点就调用当前环境下的createElement方法(在浏览器环境下就是document.createElement)来创建真实的元素节点。
将元素渲染到视图只需要调用当前环境下的appendChild方法(在浏览器环境下就是document.appendChild),就可以将一个元素插入到指定的父节点中。
元素节点如果有子节点(children),则每个子虚拟节点都执行一遍创建元素的逻辑。
注释节点
vnode中有一个唯一的标识属性isComment,为true则为注释节点。调用当前环境下的createComment方法(在浏览器环境下就是document.createComment)来创建真实的注释节点并将其插入到指定的父节点中。
文本节点
tag属性不存在、isComment属性不为true,则为文本节点。调用当前环境下的createTextNode方法(在浏览器环境下就是document.createTextNode)来创建真实的文本节点并将其插入到指定的父节点中。
下图为创建一个节点并将其渲染到视图的全过程:
部分源码如下:
createElm
/**
* 基于 vnode 创建整棵 DOM 树,并插入到父节点上
* @param vnode
* @param insertedVnodeQueue 钩子函数队列
* @param parentElm 参数vnode对应真实 DOM 对象的父节点 DOM 对象
* @param refElm 占位节点对象,例如,参数vnode对应 DOM 对象的下个兄弟节点
* @param nested 判断vnode是否是根实例的 Virtual DOM
* @param ownerArray 自身的子节点数组
* @param index 子节点数组下标
*/
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
// This vnode was used in a previous render!
// now it's used as a new node, overwriting its elm would cause
// potential patch errors down the road when it's used as an insertion
// reference node. Instead, we clone the node on-demand before creating
// associated DOM element for it.
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
/**
* 1、如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载,
* 然后为组件执行各个模块的 create 钩子
* 如果组件被 keep-alive 包裹,则激活组件
* 2、如果是一个普通元素,则什么也不做
*/
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 获取 data 对象
var data = vnode.data;
// 所有的孩子节点
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
{
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
// 创建新节点
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
// 递归创建所有子节点(普通元素、组件)
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
// 将节点插入父节点
insert(parentElm, vnode.elm, refElm);
}
if (data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {
// 注释节点,创建注释节点并插入父节点
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
// 文本节点,创建文本节点并插入父节点
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
createChildren
// 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树
function createChildren (vnode, children, insertedVnodeQueue) {
// children 是数组,表示是一组节点
if (Array.isArray(children)) {
{
// 检测这组节点的 key 是否重复
checkDuplicateKeys(children);
}
for (var i = 0; i < children.length; ++i) {
// 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
// 说明是文本节点,创建文本节点,并插入父节点
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
createComponent
/**
* 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载
* 然后为组件执行各个模块的 create 方法
* @param vnode
* @param insertedVnodeQueue 数组
* @param parentElm oldVnode 的父节点
* @param refElm oldVnode 的下一个兄弟节点
* @return 如果 vnode 是一个组件并且组件创建成功,则返回 true,否则返回 undefined
*/
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// 验证组件实例是否已经存在并且被 keep-alive 包裹
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
// 执行 vnode.data.init 钩子函数
// 如果是被 keep-alive 包裹的组件:则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
// 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
// 如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例,并挂载
// 这时候就可以给组件执行各个模块的的 create 钩子了
initComponent(vnode, insertedVnodeQueue);
// 将组件的 DOM 节点插入到父节点内
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
// 组件被 keep-alive 包裹的情况,激活组件
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
insert
// 向父节点插入节点
function insert (parent, elm, ref$$1) {
if (isDef(parent)) {
if (isDef(ref$$1)) {
if (nodeOps.parentNode(ref$$1) === parent) {
nodeOps.insertBefore(parent, elm, ref$$1);
}
} else {
nodeOps.appendChild(parent, elm);
}
}
}
invokeCreateHooks
// 调用 各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等
// 然后执行组件的 mounted 生命周期方法
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
// 组件钩子
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
// 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
删除节点
当一个节点只在oldVNode中存在时,我们需要把它从DOM中删除。
实现代码如下:
var nodeOps = Object.freeze({
parentNode: parentNode,
removeChild: removeChild
})
function parentNode (node) {
return node.parentNode
}
function removeNode (el) {
var parent = nodeOps.parentNode(el);
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
nodeOps.removeChild(parent, el);
}
}
// 移除指定索引范围(startIdx —— endIdx)内的节点
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else {
// Text node
removeNode(ch.elm);
}
}
}
}
更新节点
两个节点是同一个节点时,才需要更新元素节点。
更新过程:
- 判断新旧虚拟节点是否是静态节点,是就直接跳过;
- 新虚拟节点有文本属性,并且和旧虚拟节点的文本属性不一样时,可以直接调用setTextContent方法来将视图中DOM节点的内容改为虚拟节点(vnode)的text属性所保存的文字
- 新虚拟节点有children属性,如果oldVNode也有children属性,我们要对新旧虚拟节点的children进行一个更详细的对比并更新; 如果oldVNode没有children属性,那么说明oldVNode要么是一个空标签,要么是有文本的文本节点。如果是文本节点,先把文本清空让它变成空标签,然后将vnode中的children挨个创建成真实的DOM元素节点并将其插入到视图中的DOM节点下面
- 新虚拟节点无children属性, vnode既没有text属性也没有children属性时,这说明这个新创建的节点是一个空节点,如果oldVNode中有子节点就删除子节点,有文本就删除文本,有什么删什么,最后达到视图中是空标签的目的。
下图是更新节点的逻辑:
部分源码如下:
patchVnode
/**
* 更新节点
* 全量的属性更新
* 如果新老节点都有孩子,则递归执行 diff
* 如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
* 如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
* 更新文本节点
*/
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 老节点和新节点相同,直接返回
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
var elm = (vnode.elm = oldVnode.elm);
// 异步占位符节点
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return;
}
// 跳过静态节点的更新
// 新旧节点都是静态的而且两个节点的 key 一样,并且新节点被 clone 了 或者 新节点有 v-once指令,则重用这部分节点
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
// 执行组件的 prepatch 钩子
var i;
var data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
// 老节点的孩子
var oldCh = oldVnode.children;
// 新节点的孩子
var ch = vnode.children;
// 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
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)) {
if (oldCh !== ch) {
// 如果新老节点都有孩子,则递归执行 diff 过程
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
} else if (isDef(ch)) {
// 老孩子不存在,新孩子存在,则创建这些新孩子节点
{
checkDuplicateKeys(ch);
}
// oldVnode有文本,将文本内容置空
if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, "");
}
// 创建子节点,把vnode子节点添加到DOM中
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 老孩子存在,新孩子不存在,则移除这些老孩子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// oldVnode有文本,将文本内容置空
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);
}
}
}
addVnodes
// 在指定索引范围(startIdx —— endIdx)内添加节点
function addVnodes(
parentElm,
refElm,
vnodes,
startIdx,
endIdx,
insertedVnodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(
vnodes[startIdx],
insertedVnodeQueue,
parentElm,
refElm,
false,
vnodes,
startIdx
);
}
}
下图为源码流程图:
更新子节点
对比两个子节点列表,首先需要做的事情就是循环。循环newChildren(新子节列表),每循环一个新子节点,就去oldChildren(旧子节点列表)中找到和当前节点相同的那个旧子节点。如果在oldChildren中找不到,说明当前子节点是由于状态变化而新增的节点,我们要进行创建节点并插入视图的操作;如果找到了,就执行更新;如果找到的旧子节点的位置与新子节点不同,则执行移动节点操作。
创建子节点
如果在oldChildren中找不到本次循环所指向的新子节点相同的节点,说明当前子节点是由于状态变化而新增的节点 。对于新增节点,我们需要执行创建节点的操作,并将新创建的节点插入到oldChildren中所有未处理节点(未处理就是没有进行任何更新操作的节点)的前面。
更新子节点
当一个节点同时存在于newChildren和oldChildren中才会发生更新操作。
移动子节点
在newChildren中的某个节点和oldChildren中的某个节点是同一个节点,但是位置不同,就需要执行移动节点的操作。
patch运行流程
patch
/**
* vm.__patch__
* 1、新节点不存在,老节点存在,调用 destroy,销毁老节点
* 2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
* 3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
*/
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 如果新节点不存在,老节点存在,则调用 destroy,销毁老节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) {
invokeDestroyHook(oldVnode);
}
return;
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
// 新的 VNode 存在,老的 VNode 不存在,这种情况会在一个组件初次渲染的时候出现,比如:
// <div id="app"><comp></comp></div>
// 这里的 comp 组件初次渲染时就会走这儿
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// 判断 oldVnode 是否为真实元素
var isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// 是真实元素,则表示初次渲染
if (isRealElement) {
// 挂载到真实元素以及处理服务端渲染的情况
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode;
} else {
warn(
"The client-side rendered virtual DOM tree is not matching " +
"server-rendered content. This is likely caused by incorrect " +
"HTML markup, for example nesting block-level elements inside " +
"<p>, or missing <tbody>. Bailing hydration and performing " +
"full client-side render."
);
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 不是服务端渲染,或者 hydration 失败,则根据 oldVnode 创建一个 vnode 节点
oldVnode = emptyNodeAt(oldVnode);
}
// 拿到老节点的真实元素
var oldElm = oldVnode.elm;
// 获取老节点的父元素,即 body
var parentElm = nodeOps.parentNode(oldElm);
// 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下
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)
);
// update parent placeholder node element, recursively
// 递归更新父占位符节点元素
if (isDef(vnode.parent)) {
var ancestor = vnode.parent;
var patchable = isPatchable(vnode);
while (ancestor) {
for (var i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (patchable) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, ancestor);
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
var insert = ancestor.data.hook.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (var i$2 = 1; i$2 < insert.fns.length; i$2++) {
insert.fns[i$2]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// 移除老节点
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
}
// 判读两个节点是否相同
function sameVnode(a, b) {
return (
// key 必须相同
a.key === b.key &&
// 标签相同
((a.tag === b.tag &&
// isComment属性相同(用来判断是否是注释节点)
a.isComment === b.isComment &&
// 都有 data 属性
isDef(a.data) === isDef(b.data) &&
// input 标签的情况
sameInputType(a, b)) ||
// 异步占位符节点
(isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)))
);
}
/**
* diff 过程:
* diff 优化:做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
* 如果没有命中假设,则执行遍历,从老节点中找到新开始节点
* 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
* 如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
* 如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
*/
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
// 老节点的开始索引
var oldStartIdx = 0;
// 新节点的开始索引
var newStartIdx = 0;
// 老节点的结束索引
var oldEndIdx = oldCh.length - 1;
// 第一个老节点
var oldStartVnode = oldCh[0];
// 最后一个老节点
var oldEndVnode = oldCh[oldEndIdx];
// 新节点的结束索引
var newEndIdx = newCh.length - 1;
// 第一个新节点
var newStartVnode = newCh[0];
// 最后一个新节点
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
// removeOnly是一个特殊的标志,仅由 <transition-group> 使用,以确保被移除的元素在离开转换期间保持在正确的相对位置
var canMove = !removeOnly;
{
// 检查新节点的 key 是否重复
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)) {
// 老开始节点和新开始节点是同一个节点,执行 patch
patchVnode(
oldStartVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
// patch 结束后老开始和新开始的索引分别加 1
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 老结束和新结束是同一个节点,执行 patch
patchVnode(
oldEndVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
// patch 结束后老结束和新结束的索引分别减 1
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 老开始和新结束是同一个节点,执行 patch
// Vnode moved right
patchVnode(
oldStartVnode,
newEndVnode,
insertedVnodeQueue,
newCh,
newEndIdx
);
// 处理被 transtion-group 包裹的组件时使用
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
);
// patch 结束后老开始索引加 1,新结束索引减 1
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 老结束和新开始是同一个节点,执行 patch
// Vnode moved left
patchVnode(
oldEndVnode,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
// patch 结束后,老结束的索引减 1,新开始的索引加 1
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 遍历找到新开始节点在老节点中的位置索引
if (isUndef(oldKeyToIdx)) {
// 找到老节点中每个节点 key 和 索引之间的关系映射 => oldKeyToIdx = { key1: idx1, ... }
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 在映射中找到新开始节点在老节点中的位置索引
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
);
} else {
vnodeToMove = oldCh[idxInOld];
// 如果这两个节点是同一个,则执行 patch
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
// patch 结束后将该老节点置为 undefined
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) {
// 说明老节点被遍历完了,新节点有剩余,则说明这部分剩余的节点是新增的节点,然后添加这些节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else if (newStartIdx > newEndIdx) {
// 说明新节点被遍历完了,老节点有剩余,说明这部分的节点被删掉了,则移除这些节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
// 检查一组元素的 key 是否重复
function checkDuplicateKeys(children) {
var seenKeys = {};
for (var i = 0; i < children.length; i++) {
var vnode = children[i];
var key = vnode.key;
if (isDef(key)) {
if (seenKeys[key]) {
warn(
"Duplicate keys detected: '" +
key +
"'. This may cause an update error.",
vnode.context
);
} else {
seenKeys[key] = true;
}
}
}
}
// 在指定索引范围(startIdx —— endIdx)内添加节点
function addVnodes(
parentElm,
refElm,
vnodes,
startIdx,
endIdx,
insertedVnodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(
vnodes[startIdx],
insertedVnodeQueue,
parentElm,
refElm,
false,
vnodes,
startIdx
);
}
}
// 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... }
function createKeyToOldIdx(children, beginIdx, endIdx) {
var i, key;
var map = {};
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key;
if (isDef(key)) {
map[key] = i;
}
}
return map;
}
invokeDestroyHook
/**
* 销毁节点:
* 执行组件的 destroy 钩子,即执行 $destroy 方法
* 执行组件各个模块(style、class、directive 等)的 destroy 方法
* 如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
*/
function invokeDestroyHook(vnode) {
var i, j;
var data = vnode.data;
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.destroy))) {
i(vnode);
}
for (i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](vnode);
}
}
if (isDef((i = vnode.children))) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j]);
}
}
}
参考资料: