如何理解vue中的diff算法?
// 1. 初始化 源码 8434行
var platformModules = [attrs, klass, events, domProps, style, transition];
// 源码 6680行
var baseModules = [ref, directives];
// 源码 8447行
var modules = platformModules.concat(baseModules);
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
// 源码 9036行 将声明的patch 给到实例__path__上面
Vue.prototype.__patch__ = inBrowser ? patch : noop;
// 2. createPatchFunction
var hooks = ['create', 'activate', 'update', 'remove', 'destroy']; // 生命周期大定义
function createPatchFunction(backend) {
var i, j;
var cbs = {};
var modules = backend.modules;
var nodeOps = backend.nodeOps; // 这是真实操作dom的方法
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]]);
}
}
}
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm);
}
function createRmCb(childElm, listeners) {
function remove$$1() {
if (--remove$$1.listeners === 0) {
removeNode(childElm);
}
}
remove$$1.listeners = listeners;
return remove$$1;
}
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);
}
}
function isUnknownElement$$1(vnode, inVPre) {
return (
!inVPre &&
!vnode.ns &&
!(
config.ignoredElements.length &&
config.ignoredElements.some(function (ignore) {
return isRegExp(ignore) ? ignore.test(vnode.tag) : ignore === vnode.tag;
})
) &&
config.isUnknownElement(vnode.tag)
);
}
var creatingElmInVPre = 0;
// TODO: 当前函数 我理解是 通过vnode 来创建真实的dom 且挂载到vnode.elm上面
// 1. 先判断是否需要创建组件
// 2. 当前tag标签名是否存在 若存在则创建一个当前vnode的真实dom
// 3. 若当前vnode有scoped等设置
// 4. 再创建子级 这里可以形成一个递归调用
// 5. 若是注释 则创建注释
// 6. 最后就是文本节点创建
// 7. 从最底层的文本Insert到当前父级 递归向上一直insert到对应的父级dom当中
// 8. 若遇到组件 则开始组件的生命周期内程一套 包括处理成匿名with函数 然后一样的触发update
// 一样的执行patch函数来转化vnode到真实dom
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
// TODO: 这里当我们是一个组件时 处理完后并不需要管理后代children因为压根儿就没得child所以
// return了
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
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);
}
// TODO:将子级dom操作压入父级dom
// 比如第一次初始化得时候 就是将虚拟dom构建的真实dom压入body下面
// 此时 我们的dom上将会存在两个<div id='app'></div>
// 第一个是我们手动编写的真实dom
// 第二个是根据虚拟dom vue编译构建的真实dom
// 因此后续vue会删除原来的手动编写的dom
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);
}
}
function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
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)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true;
}
}
}
function initComponent(vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
vnode.elm = vnode.componentInstance.$el;
if (isPatchable(vnode)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
setScope(vnode);
} else {
// empty component root.
// skip all element-related modules except for ref (#3455)
registerRef(vnode);
// make sure to invoke the insert hook
insertedVnodeQueue.push(vnode);
}
}
function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
var i;
// hack for #4339: a reactivated component with inner transition
// does not trigger because the inner node's created hooks are not called
// again. It's not ideal to involve module-specific logic in here but
// there doesn't seem to be a better way to do it.
var innerNode = vnode;
while (innerNode.componentInstance) {
innerNode = innerNode.componentInstance._vnode;
if (isDef((i = innerNode.data)) && isDef((i = i.transition))) {
for (i = 0; i < cbs.activate.length; ++i) {
cbs.activate[i](emptyNode, innerNode);
}
insertedVnodeQueue.push(innerNode);
break;
}
}
// unlike a newly created component,
// a reactivated keep-alive component doesn't insert itself
insert(parentElm, vnode.elm, refElm);
}
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);
}
}
}
function createChildren(vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
// TODO: 遍历children 是否key值有效
{
checkDuplicateKeys(children);
}
for (var i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
}
} else if (isPrimitive(vnode.text)) {
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
}
}
function isPatchable(vnode) {
while (vnode.componentInstance) {
vnode = vnode.componentInstance._vnode;
}
return isDef(vnode.tag);
}
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);
}
if (isDef(i.insert)) {
insertedVnodeQueue.push(vnode);
}
}
}
// set scope id attribute for scoped CSS.
// this is implemented as a special case to avoid the overhead
// of going through the normal attribute patching process.
function setScope(vnode) {
var i;
if (isDef((i = vnode.fnScopeId))) {
nodeOps.setStyleScope(vnode.elm, i);
} else {
var ancestor = vnode;
while (ancestor) {
if (isDef((i = ancestor.context)) && isDef((i = i.$options._scopeId))) {
nodeOps.setStyleScope(vnode.elm, i);
}
ancestor = ancestor.parent;
}
}
// for slot content they should also get the scopeId from the host instance.
if (isDef((i = activeInstance)) && i !== vnode.context && i !== vnode.fnContext && isDef((i = i.$options._scopeId))) {
nodeOps.setStyleScope(vnode.elm, i);
}
}
function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
// 若式子 不成立 则表示新旧节点一致 不需要改变原dom树结构 可达复用节点
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx);
}
}
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]);
}
}
}
function removeVnodes(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);
}
}
}
}
function removeAndInvokeRemoveHook(vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
var i;
var listeners = cbs.remove.length + 1;
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners;
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners);
}
// recursively invoke hooks on child component root node
if (isDef((i = vnode.componentInstance)) && isDef((i = i._vnode)) && isDef(i.data)) {
removeAndInvokeRemoveHook(i, rm);
}
for (i = 0; i < cbs.remove.length; ++i) {
cbs.remove[i](vnode, rm);
}
if (isDef((i = vnode.data.hook)) && isDef((i = i.remove))) {
i(vnode, rm);
} else {
rm();
}
} else {
removeNode(vnode.elm);
}
}
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 is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;
{
checkDuplicateKeys(newCh); // 校验新vnode keys值
}
/**
*
* diff 思想就是:
* 1. 第一考虑: 不移动DOM
* 2. 第二考虑: 移动DOM
* 3. 第三考虑: 新建/删除 DOM
* 4. 能不移动,尽量不移动;不行就移动,实在不行就新建
*
* 所以衍生了下面5种比较逻辑
* 1. 旧头 ?= 新头 (++ newStartIdx, ++ oldStartIdx)
* 2. 旧尾 ?= 新尾 (-- newEndIdx, -- oldEndIdx)
* 3. 旧头 ?= 新尾 (++ oldStartIdx,-- newEndIdx)
* 4. 旧尾 ?= 新头 (-- oldEndIdx,++ newStartIdx])
* 5. 单个查找 (将新节点拿到旧节点里面挨个对比)(++ newStartIdx)
*
*/
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// TODO: 1. 首先判断老vnode 第一个节点是否存在
if (isUndef(oldStartVnode)) {
// 若不存在 则将第二个值给oldStartVnode
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
}
// TODO: 2. 判断老vnode 最后一个节点是否存在
else if (isUndef(oldEndVnode)) {
// 若不存在 则将倒数第二个值给oldEndVnode
oldEndVnode = oldCh[--oldEndIdx];
}
// TODO:3. 判断老vnode第一个节点与新vnode第一个节点是否一致
// ************(“旧头” 与 “新头” 比较)************
else if (sameVnode(oldStartVnode, newStartVnode)) {
// 若一致 则调用patchVnode
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
// 新头旧头比较到最后一层text节点后 让老子节点++当前 让新子节点++当前
oldStartVnode = oldCh[++oldStartIdx]; // ++ 比较旧vnode后一个vnode
newStartVnode = newCh[++newStartIdx]; // ++ 比较新vnode后一个vnode
}
// TODO: 4. 判断老vnode最后节点与新vnode最后节点是否一致
// ************(“旧尾” 与 “新尾” 比较)************
else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
}
// TODO: 5. 判断老vnode第一个节点与新vnode最后节点是否一致
// ************(“旧头” 与 “新尾” 比较)************
else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// TODO:5.1 头移动到尾 右移
// 当旧头等于新尾时 表示需要将原dom的头节点移动到原dom的最尾部
// 因为原生并没有提供插入dom到某节点后面方法 所以只有使用insertBefore方法
// 这里很巧妙的使用了当前最后节点的下一个兄弟节点表示插入到它的前面处理
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
}
// TODO: 6. 判断老vnode最后节点与新vnode第一个节点是否一致
// ************(“旧尾” 与 “新头” 比较)************
else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// TODO:6.1 尾移动到头 左移动
// 当旧尾等于新头时 表示需要将原dom的尾节点移动到原dom的最头部
// 这里直接使用insertBefore方法即可
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
}
// TODO:7. 单个查找
else {
// TODO:7.1 动态生成以key值及顺序值 构成的key唯一对象 这里很巧妙 当前得olStartIdx&oldEndIdx已经经历了前面的遍历
// 所以这个时候 这两个值并不是初始化的最大值 因此创建处理的个数并不等于数组的个数
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// TODO: 7.2 拿新节点的key 到上面生成的旧key对象中查询
// 若存在 则返回当前在旧子级中的位置
// 若不存在 则执行findIdxInOld方法 遍历旧节点所有并与当前新节点比较 若找到相同值 则返回当前索引
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
// TODO: 7.3 若既没有在旧key对象中找到 也没在旧节点中遍历找到 则认为 当前新节点为 纯新 需要 新建DOM
if (isUndef(idxInOld)) {
// New element
// 新的节点 -> 新建 -> 插入的位置默认在当前老节点的前面
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
// TODO: 7.4 若在旧节点中找到了
// 如果走key中找到 我们还没比较过sameVnode (遍历对比过 为了兼容这里再次作一次对比)
else {
vnodeToMove = oldCh[idxInOld];
// 若对比后 相同节点 则对比其子节点
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
oldCh[idxInOld] = undefined;
// 当canMove为true时 才移动该旧节点到首位节点的前面 这块没看懂 可能与transition-group的特殊情况时
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
}
// 一样的key但是不同的元素 则处理成新的元素
else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
// 这步也很关键 当单个查找 结束后 新节点索引++
newStartVnode = newCh[++newStartIdx];
}
}
// TODO:8. 在updateChildren 中,比较完新旧两个数组之后,可能某个数组会剩下部分节点没有被处理过
// 处理剩下的节点
// 比如新节点长度 < 旧节点 那么必然新节点先遍历完 则表示旧节点中需要删除多余节点
// 比如新节点长度 > 旧节点 那么必然旧节点先遍历完 则表示旧节点中需要增加新节点中多余的节点
if (oldStartIdx > oldEndIdx) {
// 这里还有个问题就是 当插入时 插入到哪个地方的处理 也就是下面的refElm定义
// refElm 获取的是 newEndIdx 后一位的节点,当前没有处理的节点是 newEndIdx
// 也就是说 newEndIdx+1 的节点如果存在的话,肯定被处理过了
// 如果 newEndIdx 没有移动过,一直是最后一位,那么就不存在 newCh[newEndIdx + 1]
// 那么 refElm 就是空,那么剩余的新节点 就全部添加进 父节点孩子的末尾
// 如果 newEndIdx 移动过,那么就逐个添加在 refElm 的前面x`
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
}
// TODO:9. 说明新节点比对完了,老节点可能还有,需要删除剩余的老节点
// 而删除老节点位置 就是老节点的头到老节点的尾
else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
function checkDuplicateKeys(children) {
// TODO:这里针对当前组件下有使用key的vNode进行 key值是否唯一判断
var seenKeys = {};
for (var i = 0; i < children.length; i++) {
var vnode = children[i];
var key = vnode.key;
// 是否有key
if (isDef(key)) {
// 当前key值 是否有对应的value值 为true
// 若有则表示当前key已被之前使用了 抛出异常 这个是我们经常会遇到的
if (seenKeys[key]) {
warn("Duplicate keys detected: '" + key + "'. This may cause an update error.", vnode.context);
} else {
// 当前key值没找到 则置为true
seenKeys[key] = true;
}
}
}
}
function findIdxInOld(node, oldCh, start, end) {
for (var i = start; i < end; i++) {
var c = oldCh[i];
if (isDef(c) && sameVnode(node, c)) {
return i;
}
}
}
// 比较两个Vnode 的子节点
function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
// TODO:若新老vnode没的变化 完全一样 则退出
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
// 将老的真实dom赋值给新的
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;
}
// 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;
}
// 这里是对比当当前vnode是一个组件时 首先data里面自带了hook
// destroy
// init
// insert
// prepatch
var i;
var data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
// 判断是否存在data -> 是否存在hook并赋值给i -> 是否存在hook.prepatch且赋值给i
// 到最后 i赋值尾hook.prepatch(oldVnode, vnode)
// 跳到源码3129 行
i(oldVnode, vnode);
}
// 拿出老vnode的子级
var oldCh = oldVnode.children;
// 拿出新vnode的子级
var ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) {
// 这个地方是根据新老vnode 分别更新
// 1. updateAttrs
// 2. updateClass
// 3. updateDOMListeners
// 4. updateDOMProps
// 5. updateStyle
// 6. update
// 7. updateDirectives
cbs.update[i](oldVnode, vnode);
}
if (isDef((i = data.hook)) && isDef((i = i.update))) {
i(oldVnode, vnode);
}
}
// TODO: 1. 判断是否存在text文本节点(意思就是是否到最底层节点)
if (isUndef(vnode.text)) {
// TODO: 1.1 是否同时存在新子节点和老子节点 且还不一样 则对比子级
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) {
// TODO: 1.1.1 这里很关键了 当判断出老vnode子级跟新vnode子级不一样时 执行
// 更新子级操作 diff算法关键来了
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
}
} else if (isDef(ch)) {
// TODO:1.2 若只存在新子节点 不存在老子节点 则再检查下key值是否异常
{
checkDuplicateKeys(ch);
}
if (isDef(oldVnode.text)) {
// 新子节点不存在 则重置老子节点文本
nodeOps.setTextContent(elm, '');
}
// TODO:1.2.1 则当前的新子节点 全部重建新dom
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// TODO: 若只存在老子节点 则老子节点全部删除
removeVnodes(oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// TODO:2. 若存在 则判断新老文本节点是否一样 不一样就更新dom的文本内容
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) {
i(oldVnode, vnode);
}
}
}
function invokeInsertHook(vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
var hydrationBailed = false;
// list of modules that can skip create hook during hydration because they
// are already rendered on the client or has no need for initialization
// Note: style is excluded because it relies on initial clone for future
// deep updates (#7063).
var isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key');
// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate(elm, vnode, insertedVnodeQueue, inVPre) {
var i;
var tag = vnode.tag;
var data = vnode.data;
var children = vnode.children;
inVPre = inVPre || (data && data.pre);
vnode.elm = elm;
if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
vnode.isAsyncPlaceholder = true;
return true;
}
// assert node match
{
if (!assertNodeMatch(elm, vnode, inVPre)) {
return false;
}
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.init))) {
i(vnode, true /* hydrating */);
}
if (isDef((i = vnode.componentInstance))) {
// child component. it should have hydrated its own tree.
initComponent(vnode, insertedVnodeQueue);
return true;
}
}
if (isDef(tag)) {
if (isDef(children)) {
// empty element, allow client to pick up and populate children
if (!elm.hasChildNodes()) {
createChildren(vnode, children, insertedVnodeQueue);
} else {
// v-html and domProps: innerHTML
if (isDef((i = data)) && isDef((i = i.domProps)) && isDef((i = i.innerHTML))) {
if (i !== elm.innerHTML) {
/* istanbul ignore if */
if (typeof console !== 'undefined' && !hydrationBailed) {
hydrationBailed = true;
console.warn('Parent: ', elm);
console.warn('server innerHTML: ', i);
console.warn('client innerHTML: ', elm.innerHTML);
}
return false;
}
} else {
// iterate and compare children lists
var childrenMatch = true;
var childNode = elm.firstChild;
for (var i$1 = 0; i$1 < children.length; i$1++) {
if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) {
childrenMatch = false;
break;
}
childNode = childNode.nextSibling;
}
// if childNode is not null, it means the actual childNodes list is
// longer than the virtual children list.
if (!childrenMatch || childNode) {
/* istanbul ignore if */
if (typeof console !== 'undefined' && !hydrationBailed) {
hydrationBailed = true;
console.warn('Parent: ', elm);
console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children);
}
return false;
}
}
}
}
if (isDef(data)) {
var fullInvoke = false;
for (var key in data) {
if (!isRenderedModule(key)) {
fullInvoke = true;
invokeCreateHooks(vnode, insertedVnodeQueue);
break;
}
}
if (!fullInvoke && data['class']) {
// ensure collecting deps for deep class bindings for future updates
traverse(data['class']);
}
}
} else if (elm.data !== vnode.text) {
elm.data = vnode.text;
}
return true;
}
function assertNodeMatch(node, vnode, inVPre) {
if (isDef(vnode.tag)) {
return vnode.tag.indexOf('vue-component') === 0 || (!isUnknownElement$$1(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()));
} else {
return node.nodeType === (vnode.isComment ? 8 : 3);
}
}
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// TODO:判断是否不存在新的vnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) {
// 是否存在旧的vnode - oldVnode
invokeDestroyHook(oldVnode); // 有旧无新 则走destroy钩子
}
return;
}
var isInitialPatch = false;
var insertedVnodeQueue = [];
// 是否不存在旧的vnode
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// 判断是否为一个node节点
var isRealElement = isDef(oldVnode.nodeType);
// 当数据变化时 oldVnode除第一次外肯定不是一个真实的dom元素
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
// 这部分 主要判断是否是SSR
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
// TODO:当第一次渲染得时候 oldVnode 指向<div id="app"></div>真实dom
// 需要转换为虚拟vNode结构数据
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
var oldElm = oldVnode.elm; // 真实根dom赋值
var parentElm = nodeOps.parentNode(oldElm); // 操作真实dom 找到body
// 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)
);
// 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;
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
};
}
// 2.1 判断是否为同一个vnode 源码 5810行
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)) ||
(isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error)))
);
}
// 源码 5827行
function sameInputType(a, b) {
if (a.tag !== 'input') {
return true;
}
var i;
var typeA = isDef((i = a.data)) && isDef((i = i.attrs)) && i.type;
var typeB = isDef((i = b.data)) && isDef((i = i.attrs)) && i.type;
return typeA === typeB || (isTextInputType(typeA) && isTextInputType(typeB));
}
// 3. patchVnode函数中 对比两个新老节点时 会进行一系列update操作
// 因为我们在updateChildren中只是去操作了dom的新建 删除 移动
// 但是每个dom上面的属性呀 事件这些都没有去操作复用 所以额外执行一系列的update操作
// 3.1 更新属性
function updateAttrs(oldVnode, vnode) {
var opts = vnode.componentOptions;
if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) {
return;
}
// 如果新老节点都没得attrs 就返回 不用比较了
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
return;
}
var key, cur, old;
var elm = vnode.elm;
var oldAttrs = oldVnode.data.attrs || {};
var attrs = vnode.data.attrs || {};
// clone observed objects, as the user probably wants to mutate it
if (isDef(attrs.__ob__)) {
attrs = vnode.data.attrs = extend({}, attrs);
}
for (key in attrs) {
cur = attrs[key];
old = oldAttrs[key];
if (old !== cur) {
setAttr(elm, key, cur);
}
}
// #4391: in IE9, setting type can reset value for input[type=radio]
// #6666: IE/Edge forces progress value down to 1 before setting a max
/* istanbul ignore if */
if ((isIE || isEdge) && attrs.value !== oldAttrs.value) {
setAttr(elm, 'value', attrs.value);
}
for (key in oldAttrs) {
if (isUndef(attrs[key])) {
if (isXlink(key)) {
elm.removeAttributeNS(xlinkNS, getXlinkProp(key));
} else if (!isEnumeratedAttr(key)) {
elm.removeAttribute(key);
}
}
}
}
// 3.2 更新css类
function updateClass(oldVnode, vnode) {
var el = vnode.elm;
var data = vnode.data;
var oldData = oldVnode.data;
if (isUndef(data.staticClass) && isUndef(data.class) && (isUndef(oldData) || (isUndef(oldData.staticClass) && isUndef(oldData.class)))) {
return;
}
var cls = genClassForVnode(vnode);
// handle transition classes
var transitionClass = el._transitionClasses;
if (isDef(transitionClass)) {
cls = concat(cls, stringifyClass(transitionClass));
}
// set the class
if (cls !== el._prevClass) {
el.setAttribute('class', cls);
el._prevClass = cls;
}
}
// 3.3 更新dom事件监听
function updateDOMListeners(oldVnode, vnode) {
// 若新老均没有事件绑定 则返回
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return;
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm;
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
target$1 = undefined;
}
// 3.3.1
// normalize v-model event tokens that can only be determined at runtime.
// it's important to place the event as the first in the array because
// the whole point is ensuring the v-model callback gets called before
// user-attached handlers.
function normalizeEvents(on) {
/* istanbul ignore if */
if (isDef(on[RANGE_TOKEN])) {
// IE input[type=range] only supports `change` event
var event = isIE ? 'change' : 'input';
on[event] = [].concat(on[RANGE_TOKEN], on[event] || []);
delete on[RANGE_TOKEN];
}
// This was originally intended to fix #4521 but no longer necessary
// after 2.5. Keeping it for backwards compat with generated code from < 2.4
/* istanbul ignore if */
if (isDef(on[CHECKBOX_RADIO_TOKEN])) {
on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []);
delete on[CHECKBOX_RADIO_TOKEN];
}
}
// 3.3.2
function updateListeners(on, oldOn, add, remove$$1, createOnceHandler, vm) {
var name, def$$1, cur, old, event;
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
warn('Invalid handler for event "' + event.name + '": got ' + String(cur), vm);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
// 3.4 更新dom Props
function updateDOMProps(oldVnode, vnode) {
if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
return;
}
var key, cur;
var elm = vnode.elm;
var oldProps = oldVnode.data.domProps || {};
var props = vnode.data.domProps || {};
// clone observed objects, as the user probably wants to mutate it
if (isDef(props.__ob__)) {
props = vnode.data.domProps = extend({}, props);
}
for (key in oldProps) {
if (!(key in props)) {
elm[key] = '';
}
}
for (key in props) {
cur = props[key];
// ignore children if the node has textContent or innerHTML,
// as these will throw away existing DOM nodes and cause removal errors
// on subsequent patches (#3360)
if (key === 'textContent' || key === 'innerHTML') {
if (vnode.children) {
vnode.children.length = 0;
}
if (cur === oldProps[key]) {
continue;
}
// #6601 work around Chrome version <= 55 bug where single textNode
// replaced by innerHTML/textContent retains its parentNode property
if (elm.childNodes.length === 1) {
elm.removeChild(elm.childNodes[0]);
}
}
if (key === 'value' && elm.tagName !== 'PROGRESS') {
// store value as _value as well since
// non-string values will be stringified
elm._value = cur;
// avoid resetting cursor position when value is the same
var strCur = isUndef(cur) ? '' : String(cur);
if (shouldUpdateValue(elm, strCur)) {
elm.value = strCur;
}
} else if (key === 'innerHTML' && isSVG(elm.tagName) && isUndef(elm.innerHTML)) {
// IE doesn't support innerHTML for SVG elements
svgContainer = svgContainer || document.createElement('div');
svgContainer.innerHTML = '<svg>' + cur + '</svg>';
var svg = svgContainer.firstChild;
while (elm.firstChild) {
elm.removeChild(elm.firstChild);
}
while (svg.firstChild) {
elm.appendChild(svg.firstChild);
}
} else if (
// skip the update if old and new VDOM state is the same.
// `value` is handled separately because the DOM value may be temporarily
// out of sync with VDOM state due to focus, composition and modifiers.
// This #4521 by skipping the unnecesarry `checked` update.
cur !== oldProps[key]
) {
// some property updates can throw
// e.g. `value` on <progress> w/ non-finite value
try {
elm[key] = cur;
} catch (e) {}
}
}
}
// 3.5 更新style
function updateStyle(oldVnode, vnode) {
var data = vnode.data;
var oldData = oldVnode.data;
if (isUndef(data.staticStyle) && isUndef(data.style) && isUndef(oldData.staticStyle) && isUndef(oldData.style)) {
return;
}
var cur, name;
var el = vnode.elm;
var oldStaticStyle = oldData.staticStyle;
var oldStyleBinding = oldData.normalizedStyle || oldData.style || {};
// if static style exists, stylebinding already merged into it when doing normalizeStyleData
var oldStyle = oldStaticStyle || oldStyleBinding;
var style = normalizeStyleBinding(vnode.data.style) || {};
// store normalized style under a different key for next diff
// make sure to clone it if it's reactive, since the user likely wants
// to mutate it.
vnode.data.normalizedStyle = isDef(style.__ob__) ? extend({}, style) : style;
var newStyle = getStyle(vnode, true);
for (name in oldStyle) {
if (isUndef(newStyle[name])) {
setProp(el, name, '');
}
}
for (name in newStyle) {
cur = newStyle[name];
if (cur !== oldStyle[name]) {
// ie9 setting to null has no effect, must use empty string
setProp(el, name, cur == null ? '' : cur);
}
}
}
// 3.6 更新ref
var ref = {
create: function create(_, vnode) {
registerRef(vnode);
},
update: function update(oldVnode, vnode) {
// 当新老ref 不一致 需要注册且更新ref
if (oldVnode.data.ref !== vnode.data.ref) {
registerRef(oldVnode, true);
registerRef(vnode);
}
},
destroy: function destroy(vnode) {
registerRef(vnode, true);
},
};
// 3.6.1 注册ref
function registerRef(vnode, isRemoval) {
var key = vnode.data.ref;
if (!isDef(key)) {
return;
}
var vm = vnode.context;
var ref = vnode.componentInstance || vnode.elm;
var refs = vm.$refs;
if (isRemoval) {
if (Array.isArray(refs[key])) {
remove(refs[key], ref);
} else if (refs[key] === ref) {
refs[key] = undefined;
}
} else {
if (vnode.data.refInFor) {
if (!Array.isArray(refs[key])) {
refs[key] = [ref];
} else if (refs[key].indexOf(ref) < 0) {
// $flow-disable-line
refs[key].push(ref);
}
} else {
refs[key] = ref;
}
}
}
// 3.7 更新指令
function updateDirectives(oldVnode, vnode) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode);
}
}
// 3.7.1 内部更新指令逻辑
function _update(oldVnode, vnode) {
var isCreate = oldVnode === emptyNode;
var isDestroy = vnode === emptyNode;
var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);
var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);
var dirsWithInsert = [];
var dirsWithPostpatch = [];
var key, oldDir, dir;
for (key in newDirs) {
oldDir = oldDirs[key];
dir = newDirs[key];
if (!oldDir) {
// new directive, bind
callHook$1(dir, 'bind', vnode, oldVnode);
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir);
}
} else {
// existing directive, update
dir.oldValue = oldDir.value;
dir.oldArg = oldDir.arg;
callHook$1(dir, 'update', vnode, oldVnode);
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir);
}
}
}
if (dirsWithInsert.length) {
var callInsert = function () {
for (var i = 0; i < dirsWithInsert.length; i++) {
callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);
}
};
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert);
} else {
callInsert();
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', function () {
for (var i = 0; i < dirsWithPostpatch.length; i++) {
callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
}
});
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
}
}
}
}
// 4. diff中 根据key生成类map格式数据(其实就是以key值为k 然后遍历顺序index为v)
// 如 key:'i am key 1' key:'i am key2'
// {'i am key 1':1,'i am key2':2}
// 所以这里看出来了 在某些式子中key值唯一性的重要性 就是为了diff比较
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;
}