# 组件挂载
组件挂载的过程相信大家一定很好奇,那么本篇将对组件挂载的实现进行剖析 我们知道Vue挂载是在_init的最后调用的$mount方法, 我们从$mount函数入手
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
$mount实际上是去调用mountComponent函数, 我们来看下mountComponent函数
# mountComponent
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
...
callHook(vm, 'beforeMount');
var updateComponent;
/* istanbul ignore if */
if (config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;
mark(startTag);
var vnode = vm._render();
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
我们看下mountComponent, 大家肯定注意到了vm._update(vm._render(), ...)函数(hydrating是服务端渲染时用的), 该函数在mountComponent里频繁出现, 该函数就是挂载组件的函数,这里有个Watcher实例,我们先放一边等之后介绍数据响应章节会详细介绍,这里我们只需要知道vm._update(vm._render(), ...)函数调用即可
# _render
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self
var vnode;
try {
// There's no need to maintain a stack becaues all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm;
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
handleError(e, vm, "render");
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);
} catch (e) {
handleError(e, vm, "renderError");
vnode = vm._vnode;
}
} else {
vnode = vm._vnode;
}
} finally {
currentRenderingInstance = null;
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0];
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
);
}
vnode = createEmptyVNode();
}
// set parent
vnode.parent = _parentVnode;
return vnode
};
以上高亮的代码就是_render函数的核心,它调用了render函数,正如我们在组件Vnode章节时讲的createElement函数会生成Vnode, 所以此时render出来的是根节点的vnode
# _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) {
// console.log('1-prevVnode', prevVnode);
// console.log('2-prevEl', prevEl);
// 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.
};
我们拿着render函数渲染出来的根节点vnode传入_update函数里, 现在我们只考虑初始化状态, 此时vm.vnode是为空, 所以我们调用的是if语句里的__patch_, 组件此时是不具备$el属性(只有Vue实例具有)
# patch
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });
...
Vue.prototype.__patch__ = inBrowser ? patch : noop;
function createPatchFunction() {
...
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
// console.log('3-oldVnode', oldVnode);
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
...
}
return vnode.elm
}
}
__patch__就是createPatchFunction返回出来的patch函数(createPatchFunction后面会单独拎出来讲到) 我们继续来看__patch__函数调用, 初始化会调用到createElm函数
# createElm
fnction 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)) {
...
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);
}
}
createElm的作用就是将虚拟Dom真正的变成Dom节点, 我们来分析下createElm具体实现, 抛开createCompoent函数, 我们发现后面的vnode.elm = nodeOps.createElement(tag, vnode), 此时vnode.elm就等于真实DOM节点, 调用createChildren循环children并且调用createElm进行反复创建, insert函数是将当前渲染的Dom节点插入到父级节点里, 还记得我们刚才说的_update里的$el吗, 组件初始化是不具备$el的, 所以根节点在这里是不会插入到任何节点里, createChildren调用createElm传入的parentElm则是父节点, 因为父节点先于子节点创建, 最后会变成以下样子
rootDom -> childrenDom -> grandChildrenDom -> ...
# createComponent
现在我们知道普通虚拟DOM节点是怎么创建的, 那么回过头来我们再讲下该章节最重要的一个函数, createComponent函数, 刚才我们在说createElm的时候略过了它, 现在重点讲解下该函数, 该函数定义在createPatchFunction里, 与之前的创建组件Vnode不是同一个函数
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
}
}
}
还记得之前我们在创建组件Vnode的时候合并组件钩子函数吗, 在这里我们将调用第一个组件钩子函数init函数
# hook.init
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
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);
}
}
我们讲解下非Keep-alive的情况, 首先我们会去调用createComponentInstanceForVnode去初始化组件
function createComponentInstanceForVnode (
vnode, // we know it's MountedComponentVNode but flow doesn't
parent // activeInstance in lifecycle state
) {
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)
}
以上高亮代码是为了能够记住组件初始化时传入的options是有parent属性, parent = activeInstance, activeInstance是个全局变量, 我们先看下activeInstance
function setActiveInstance(vm) {
var prevActiveInstance = activeInstance;
activeInstance = vm;
return function () {
activeInstance = prevActiveInstance;
}
}
以上代码是在_update中先于__patch__函数触发, 此时activeInstance是当前渲染的组件, 也就是createComponent对应的component的父级, 举例说明
Parent:
<div>
<Children>
</div>
此时activeInstance就是Parent,而我们正在执行Children的init钩子函数(为什么是全局activeInstance, 这个问题可以想想看)。搞清楚activeInstance之后我们再来看下初始化组件Vnode, 回顾下之前Vue.prototype._init函数
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
}
此时options就是刚才我们看到的options, options._isComponent为ture,那么我们就调用initInternalComponent函数
# initInternalComponent
function initInternalComponent (vm, options) {
var opts = vm.$options = Object.create(vm.constructor.options);
// doing this because it's faster than dynamic enumeration.
var parentVnode = options._parentVnode;
opts.parent = options.parent;
opts._parentVnode = parentVnode;
var vnodeComponentOptions = parentVnode.componentOptions;
opts.propsData = vnodeComponentOptions.propsData;
opts._parentListeners = vnodeComponentOptions.listeners;
opts._renderChildren = vnodeComponentOptions.children;
opts._componentTag = vnodeComponentOptions.tag;
if (options.render) {
opts.render = options.render;
opts.staticRenderFns = options.staticRenderFns;
}
}
我们可以看到组件是在这里赋值vm.$options, 还记得propsData吗?我们在生成组件vnode的时候根据data.props与options.props结合生成的propsData, 在这里将赋值给vm.$options, vm.constructor.options就是之前Vue.extend出来的Sub子类的options,Sub.options是由extendOptions与Vue.options结合的
# $mount函数
实例化Vue组件后将调用vm.$mount函数,这与本章一开始是一致的,就是将组件的根节点到最底部节点生成一个DOM Tree
# initComponent
回到createComponent函数, 我们调用完init后, 此时vnode.componentInstance就已经具备, 那么就会调用initComponent函数,我们来具体看下该函数的实现
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是组件vnode, 别搞混了, vnode.componentInstance.$el则是该组件patch出来的根节点真实Dom节点,还记得刚才_update函数的调用吗? vm.$el = vm.patch(...), __patch__函数返回的是根节点的真实节点, invokeCreatedHooks我会放到createPatchFunction里讲。 执行完initComponent函数后就会执行insert函数, 此时是将组件插入到父级节点里, 这里要分情况去讲:
- 组件是根节点
<!-- Parent -->
<template>
<Children />
</template>
此时Children将不会插入到任何父节点里, 此时Parent.$el就是指向Children的根节点, 那么Parent将会带着Children插入到对应的Parent的父级节点里
- 父级是html节点
<!-- Parent -->
<template>
<div>
<Children />
</div>
</template>
我们知道createElm的时候会调用普通dom节点createChildren去循环调用createElm, 那么Children就是div的子节点,div就是Children的parentElm, Children会将根节点插入到div下形成一个闭环
# 总结
本章节说的其实挺多的,从$mount挂载函数介绍到patch函数的调用到最后的组件挂载插入到dom节点里, 仔细再回顾下这章节的内容
# 追问
疑问一
- activeInstance为什么是全局变量?
