首先,需要明确,原生节点和组件节点被添加的时机是不一样的,虽然都是在Vue中调用了insert方法,但是调用insert的时机完全不一样
其次,我们需要关注VNode上的elm属性,这个属性代表了VNode对应的真实节点,这个属性只有被赋值了,才可能insert到parent上去
再其次,Vue依次创建父子组件时,真实节点是如何被挨个添加到父元素上,这个过程触发了哪些hook,这个触发的顺序也是值得研究的
原生节点
对于原生节点来说,vnode上的elm属性添加的时机是
在new Watcher() -> watcher.get() -> vm.updateComponent() -> vm.render -> vm.update -> patch -> createElm里面
举例来说:
<div id="app">{{abc}}</div>
const vm = new Vue({
el: '#app',
data: {
abc: '123'
},
});
该案例的render方法为:
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(abc))])}
})
接下来调用_c走到_createElement里面后,其创建的VNode节点elm属性为undefined 在patch里面,可以看到调用createElm时需要的parentElm、refElm两个参数是通过oldVnode.elm定位得到的:
// oldVnode此时是真实节点#app
oldVnode = emptyNodeAt(oldVnode);
var oldElm = oldVnode.elm;
var parentElm = nodeOps.parentNode(oldElm)
createElm(
vnode,
insertedVnodeQueue,
parentElm,
nodeOps.nextSibling(oldElm)
);
在此处parentElm就是body,nodeOps.nextSibling(oldElm)是一个文本节点
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
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(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);
在createElm方法中,我们可以看到根据vnode创建出来的真实节点会赋值给vnode.elm 接下来在创建完children后,就把vnode.elm插入到了parentElm中去
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
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);
}
再顺便说一下createChildren的过程:
function createChildren (vnode, children, insertedVnodeQueue) {
if (Array.isArray(children)) {
{
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)));
}
}
由于执行到createChildren(vnode, children, insertedVnodeQueue);时,vnode上已经有elm属性了,所以在createChildren里面再调用createElm时,就很自然地以vnode.elm作为parent
组件节点
原生节点创建并插入的流程就分析到这里,接下来我们看组件节点,我们以下面的案例来说明:
<div id="app">
<my :data="abc"></my>
</div>
<script src="../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
abc: '123' // 属性是在父组件中定义的传递给了子组件 (父组件的定义的数据 已经是响应式的了)
},
components: {
my: {
props: {
data: {type:String}
},
template: '<div>my-component {{data}}</div>'
}
}
});
</script>
沿着new Watcher() -> watcher.get() -> vm.updateComponent() -> vm.render -> vm.update -> patch -> createElm的流程,又走到了createElm里面,但是走到createComponent的判断之后,就到进入该分支中了:
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
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
注意,此时还没有到给vnode.elm赋值的时机,所以可以猜测,给vnode.elm赋值为真实dom节点,以及insert方法的执行,都在createComponent里面:
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 */); // init 走完以后
}
// 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); // 直接将缓存的dom元素插入
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
可以看到如果命中if (isDef(vnode.componentInstance)) 这个条件,就会执行insert(parentElm, vnode.elm, refElm),现在问题来了: 1、什么时候能命中这个if判断 2、vnode.elm是怎么被赋值的
对于第一个问题,我们可以看到在该if判断的上方执行了vnode.data.hook.init方法,可以猜测,在这个初始化方法内部给vnode加了componentInstance属性,我们不妨打开init方法看一下:
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
// 如果缓存过 则直接走 prepatch 不走初始化流程了
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance // 这个是父组件的实例
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
可以看到,确实如我们预想的一样,在vnode上加了componentInstance这个属性,其值指向组件实例,第1个问题就解决了 第2个问题,在createComponent中,进入initComponent后,可以看到,给vnode.elm赋了值:
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);
}
}
这个值是从vnode.componentInstance.el这个值,看看它又从哪里来,我们先来进入createComponentInstanceForVnode看一下它的创建过程:
function createComponentInstanceForVnode (
// we know it's MountedComponentVNode but flow doesn't
vnode,
// activeInstance in lifecycle state
parent
) {
var options = {
_isComponent: true,
_parentVnode: vnode,
parent: parent
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
return new vnode.componentOptions.Ctor(options)
}
这个里面并没有什么特别的,看起来就是用Vue的子类实例化了一个对象,因此看起来不太像是这里,不过componentVNodeHooks.init里面用child变量接了一下最后返回的实例,并调用child.mount的实参vnode.elm是undefined,所以在挂载过程中,沿着下列执行路径: child.$mount(undefined) -> mountComponent(this, el, hydrating) -> new Watcher -> vm._render -> vm._update -> patch -> createElm执行
这次进入createElm里面是要创建组件节点里的DOM元素,但在执行insert(parentElm, vnode.elm, refElm)时,由于parentElm是undefined,所以无法将创建出来的真实节点插入到parentElm中,等到这次createElm执行结束后,在之后依次返回到各级函数过程中,返回到_update的时候,会给当前Vue实例对象上赋$el,即刚刚创建出来的真实DOM节点对象:
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm); // 每个组件渲染的时候 会将当前组件的实例暴露到全局上 Dep.target
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 */); // 将组件渲染后的元素赋予给vm.$el
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
再继续返回后child.$mount执行完毕,componentVNodeHooks.init也就执行完了,然后就回到了createComponent方法里面:
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 */); // init 走完以后
}
// 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); // 直接将缓存的dom元素插入
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
再通过isDef(vnode.componentInstance)这一条件,走到initComponent方法中,就会将vnode.componentInstance.$el赋值给vnode.elm
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;
再执行insert(parentElm, vnode.elm, refElm)时,vnode.elm也已经有值,就将它插入成功了