Vue.js 的一个核心思想是组件化,就是把页面拆分成多个组件 (component)。我今天将从源码的角度来分析 Vue 是如何创建,工作的。
<script type="x-template" id="aaa">
<div>
子组件->{{msg}}
</div>
</script>
<div id="box">
<my-aaa></my-aaa>
</div>
<script>
var vm = new Vue({
el: '#box',
components: {
'my-aaa': {
template: '#aaa',
data() {
return {
msg: 'welcome vue'
}
},
}
}
});
</script>
在vue mount的时候,会首先调用 render函数生成vnode,
export function mountComponent ( vm, el, hydrating ){
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
上面vue的创建过程,会生成如下的render函数
ƒ anonymous() {
with(this) {
return _c('div', {
attrs: {
"id": "box"
}
}, [_c('my-aaa')], 1)
}
}
VNode 阶段
在调用render函数生成VNode的时候,_c(即 createElement),它最终会调用_createElement 方法
_createElement 会调用resolveAsset 验证 tag 是不是组件。如果是组件,那么通过createComponent 方法创建一个组件的 VNode。这 createComponent 是创建子组件的关键
else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag);
}
function resolveAsset(
options, // options = {components: { my-aaa:{} }}}
type, // type = "components"
id, // id = "my-aaa"
warnMissing
) {
if(typeof id !== 'string') { return }
var assets = options[type];
if(hasOwn(assets, id)) {
return assets[id]
}
}
createComponent
function createComponent (
Ctor, // Ctor = {template: "#aaa", data: ƒ}
data, // data = undefined
context, // content = vm
children, // children = undefined
tag // tag = "my-aaa"
) {
if (isUndef(Ctor)) {
return
}
var baseCtor = context.$options._base;
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
if (typeof Ctor !== 'function') {
return
}
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
var listeners = data.on;
installComponentHooks(data);
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
asyncFactory
);
return vnode
}
context.$options._base 会指向 Vue的构造函数,Vue.extend 会生成一个新的构造函数。可以看我写的 Vue.extend 是怎样实现的
var baseCtor = context.$options._base;
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor);
}
installComponentHooks就是把 componentVNodeHooks的钩子函数合并到data.hook中,,在合并过程中,如果某个时机的钩子已经存在data.hook中,那么通过执行mergeHook函数做合并勾子。
function installComponentHooks (data) {
var hooks = data.hook || (data.hook = {});
for (var i = 0; i < hooksToMerge.length; i++) {
var key = hooksToMerge[i];
var existing = hooks[key];
var toMerge = componentVNodeHooks[key];
if (existing !== toMerge && !(existing && existing._merged)) {
hooks[key] = existing ? mergeHook$1(toMerge, existing) : toMerge;
}
}
}
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
},
prepatch: function prepatch (oldVnode, vnode) {
},
insert: function insert (vnode) {
},
destroy: function destroy (vnode) {
}
};
最终生成的VNode 如下
{
children: [{
children: undefined,
componentInstance: undefined,
componentOptions: {
propsData: undefined,
listeners: undefined,
tag: "A",
children: undefined,
Ctor: ƒ
},
data: {
hook: {
init: ƒ,
prepatch: ƒ,
insert: ƒ,
destroy: ƒ
}
on: undefined
},
elm: undefined,
parent: undefined,
tag: "vue-component-1-A",
text: undefined
}],
componentInstance: undefined,
componentOptions: undefined,
data: undefined,
elm: undefined,
isStatic: false,
parent: undefined,
tag: "div",
text: undefined
}
Patch 阶段
function patch(oldVnode, vnode, hydrating, removeOnly) {
const isRealElement = isDef(oldVnode.nodeType)
if(!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
createElm()
}
}
createComponent
在执行createComponent 方法的时候,会先执行 componentVNodeHooks 钩子里面的 init 方法,这会为 vnode.componentInstance 赋值。 执行createComponentInstanceForVnode 会返回一个新的 VueComponent
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
}
export function createComponentInstanceForVnode ( vnode , parent ){
const options: InternalComponentOptions = {
_isComponent: true,
_parentVnode: vnode,
parent
}
// check inline-template render functions
const inlineTemplate = vnode.data.inlineTemplate
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render
options.staticRenderFns = inlineTemplate.staticRenderFns
}
return new vnode.componentOptions.Ctor(options)
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const 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
}
}
}
在调用init钩子函数之后,如果vnode对应的是一个子组件,然后把 vnode.elm 插到父Dom中。
componentOptions
在创建vnode的时候会传人一个componentOptions
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
componentOptions里面会有以下几个属性
{
Ctor: ƒ VueComponent(options)
children: (3) [VNode, VNode, VNode]
listeners: undefined
propsData: undefined
tag: "my-aa"
}
Ctor 其实是一个构造函数
const Sub = function VueComponent (options) {
this._init(options)
}
children其实来自于render函数