前言
上回我们了解了
vnode
从创建到生成的流程,这回我们来探索Vue
是如何将vnode
转化成真实的dom
节点 / 元素
Vue.prototype._update
上次我们提到的 _render
函数其实作为 _update
函数的参数传入,换句话说,_render
函数结束后 _update
将会执行????
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
restoreActiveInstance();
// update __vue__ reference
if (prevEl) {prevEl.__vue__ = null;}
if (vm.$el) {vm.$el.__vue__ = vm;}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el;}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
};
简单梳理下这段代码的逻辑:
- 调用
setActiveInstance(vm)
设置当前的vm
为活跃的实例 - 判断
preVnode
是否存在,是则调用vm.$el = vm.__patch__(prevVnode, vnode);
,否则调用vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
(其实也就是第一次渲染跟二次更新的区别) - 调用
restoreActiveInstance()
重置活跃的实例 - 对
HOC
做了特殊判断(因为没用过HOC
,所以这里直接略过)
从上面整理下来的逻辑中,我们能得到讯息仅仅只有 setActiveInstance
函数返回一个闭包函数(当然这并不是很重要), 如果需要更深入的了解,还需要了解 __patch__
函数是怎么实现的
其他相关代码:
updateComponent = function () {vm._update(vm._render(), hydrating);
};
...
new Watcher(vm, updateComponent, noop, {before: function before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
__patch__
说出来你可能不信,__patch__
函数的实现其实很简单????
var patch = createPatchFunction({nodeOps: nodeOps, modules: modules});
...
Vue.prototype.__patch__ = inBrowser ? patch : noop;
很明显,createPatchFunction
也是返回了一个闭包函数
patch
虽然 __patch__
外表看起来很简单,但是其实内部实现的逻辑还是挺复杂的,代码量也非常多????
return function patch (oldVnode, vnode, hydrating, removeOnly) {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
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {var isRealElement = isDef(oldVnode.nodeType);
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.
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 if (process.env.NODE_ENV !== 'production') {
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
oldVnode = emptyNodeAt(oldVnode);
}
// replacing existing element
var oldElm = oldVnode.elm;
var 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)
);
// 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(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
这么多的代码,一下子肯定是消化不完的,所以我们可以尝试性的带着以下这几个问题来看????
- 第一次的
patch
操作与后续的patch
操作有何区别? -
dom
节点之间产生变更,或者说是「新节点」替换「老节点」时,规则是怎么样的?
patch
函数的特殊逻辑
针对初次渲染,patch
函数是做了特殊逻辑的。显然我们只要把初次执行的 patch
的逻辑走一遍就清楚了????
结合上面的源码,归纳下这里的思路:
- 若「老节点」为空,则调用
createElm(vnode, insertVnodeQueue)
来 直接创建「新节点」 -
若「老节点」为真实存在的
dom
节点,则分成以下几步:- 移除「老节点」的
SSR_ATTR
属性(若存在) -
判断是否正在「渲染」(
hydrating
)-
是则执行
hydrate(oldvnode, vnode, insertVnodeQueue)
并判断是否执行成功- 成功后触发
invokeInsertHook(vnode, insertVnodeQueue, true)
- 失败后发出「警告」(测试环境)
- 成功后触发
- 否则调用
emptyNodeAt(oldVnode)
,给「老节点」(实际上是dom
节点)生成它的 “vnode
“
-
- 移除「老节点」的
被「遗忘」的一行代码
看完源码的同学不难不发现,上面梳理的逻辑里少了这段代码:
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
}
也就是对「非 dom
元素的相同节点」做一次 patchVnode
的操作。关于这段代码可以分成几点来分析:
- 什么是「相同节点」?
-
patchVnode
做了什么?
「相同的节点」
根据语义我们应该看这部分代码????
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)
)
)
)
}
sameVnode
的逻辑就是:按照 vnode
的属性来判断两个「vnode
」节点是否是同一个节点
patchVnode
由于执行 patchVnode
的前提就是新老节点是「相同」的节点,我们有理由相信,它是用来处理同个节点的变化。
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
}
// 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
}
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;
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) {updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
} else if (isDef(ch)) {if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(ch);
}
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);
} 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); }
}
}
我们看看这段代码都做了哪些事情:
- 复用
vnode
(如果存在elem
属性) - 处理异步组件
- 处理静态节点
- 执行
prepatch
(如果存在data
属性) - 执行
update
(如果存在data
属性) - 比较
oldVnode
和vnode
两个节点 - 执行
postpatch
(如果存在data
属性)
当然,这里最直观的就是比较 oldVnode
和 vnode
两个节点的逻辑????
其他的逻辑可以留到下一篇文章再分析~