在Vue中,组件分为全局组件和局部组件,首先先看看全局组件Vue是怎么注册的。
全局组件
在Vue中其实内置了很多全局组件,在init做option合并的时候我们可以看到Vue内置组件,比如keep-live、transition
具体调试位置是init时候,把组件options和Vue的option合并时候
我们注册全局组件的时候都是使用如下方式注册:
const Hello = Vue.component('Hello',{
data: function () {
return {
word: 'hello World'
}
},
template:`<h1>{{word}}</h1>`
})
Vue添加component方法时候是在注册全局api的时候
src\core\global-api\index.js
export function initGlobalAPI (Vue: GlobalAPI) {
······
initAssetRegisters(Vue)
}
initAssetRegisters(Vue)
src\core\global-api\assets.js
这里遍历了 常量ASSET_TYPES,ASSET_TYPES是什么?
src\shared\constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
假设遍历type为component,下面逻辑就是:
判断definition是否存在,当type是component的时候,且definition是对象的时候则获取name属性,没有就默认是id,同时我们知道这个this.options._base其实就是最外层大Vue的实例,所以this.options._base.extend(definition) =》 Vue.extend(definition),创建了一个vue的子类构造器,并且最后把子类构造器赋值给this.options[type + ‘s’][id] ,也就是说:
const Hello = Vue.component('Hello',{
data: function () {
return {
word: 'hello World'
}
},
template:`<h1>{{word}}</h1>`
})
// 最后会变成
Vue.options.components.Hello
所以当我们在执行init的merge的时候,把 Sub.options.components 合并到 vm.$options.components上。
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
在_createElement时候有个逻辑判断,拿到组件$option.components的tag对应的值作为Ctor,那么这个Ctor其实就是组件的构造函数传入createComponent
这和createComponent里面有个逻辑是一样的,我们如果是自定义的组件传入的Ctor是对象则会用Vue.extend()转化成构造函数,如果我们有了这个构造函数,那么就不会再extend一遍,因为我们之前已经做过了。
再看看这个resolveAsset 做了什么?
其实就是从vm.$options.components[tag]拿组件构造函数,并且如果直接拿id拿不到就会转换成驼峰/首字母大写等方式来拿,如果都拿不到则报错。这意味着我们在写Vue.component(id,definition)时候,这个组件的id我们可以是驼峰也可以是首字母大写形式。
export function resolveAsset (
options: Object,
type: string,
id: string,
warnMissing?: boolean
): any {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
const assets = options[type]
// check local registration variations first
if (hasOwn(assets, id)) return assets[id]
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) return assets[camelizedId]
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}
局部组件
局部组件我们使用时候是这样的:
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
所以我们局部组件是以option形式传入,那么就是会在_init的merge的时候合并到vm.$options.components,局部组件注册还是相对比较简单的,这里就不细看了。
组件生命周期
在_init函数中我们可以看到,代码中调用了beforeCreate 和 created
调用的方法都是通过callHook()方式,先看看callHook是怎么实现的
src\core\instance\lifecycle.js
在最底部就能看到callHook的实现,实现方式非常简单,就是通过vm.$options[hook]拿到一个钩子函数的数组,递归的调用。那么问题来了,我们平时写的不都是这样写的吗?怎么会是一个数组呢?答案在下面。
created () {
console.log('created')
}
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
beforeCreate & created
beforeCreate 和 created 函数都是在实例化 Vue 的阶段,在 _init 方法中执行的。
我们可以看到,beforeCreate的执行是在初始化render event lifecycle之后就执行的,此时我们还没有执行到initState,initState是初始化我们的date和props等数据,所以我们在beforeCreate中访问不到data就是这个原因。
created是在initState和initProvide之后执行,此时所有数据都添加完毕,可以正常访问到。
根据我们之前了解,是先执行父组件init,在patch时候有子组件才会再执行一遍子组件的_init,所以beforeCreate 和 created 是先执行父组件的,再执行子组装件的
Vue.prototype._init = function (options?: Object) {
// ...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ...
}
beforeMount & mounted
这两个声明周期钩子,是发生在mount,就是在挂载时候调用。
beforeMount是在mountComponent函数中执行,看到代码最后部分,有个判断vm.$vnode == null,当条件成立的时候才执行mounted钩子函数。
vm.$vnode是什么?
mounte之前我们经历过update patch render等过程
在_render中我们可以看到定义,所以vm.$vnode就是我们的父的Vnode,在我们初始化过程中也就是new Vue({el:‘#app’})的时候,我们的父的Vnode是没有的所以就会走到这个mounted钩子函数,那么我们组件的mounted是在哪里呢?
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
……
}
callHook(vm, 'beforeMount')
// 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
}
组件mounted在哪里定义?
之前我们看组件patch的时候,在patch方法最后有一个invokeInsertHook方法。
这个方法中我们是把所有子组件的insert方法都执行一遍,那么insert方法又是哪里来的呢,就是我们组件在createComponent的时候添加到,之前章节说过。在insert方法里面我们可以看到,执行了组件的mounted钩子函数。
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 (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
// vue-router#1212
// During updates, a kept-alive component's child components may
// change, so directly walking the tree here may call activated hooks
// on incorrect children. Instead we push them into a queue which will
// be processed after the whole patch process ended.
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true /* direct */)
}
}
},
在invokeInsertHook 中,我们发现是按参数queue来执行组件的insert方法的,那么这个queue是怎么组成的呢?
上一章节讲到的createElm中,是创建元素节点的。在createElm中:
1、在创建组件化vnode.data的时候我们都有为其添加hook所以会进入到invokeCreateHooks中,在里面我们就会为insertedVnodeQueue添加vnode实例
const data = vnode.data
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](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)
}
}
2、在createComponent的判断中,我们执行了initComponent方法,在initComponent中,我们也执行了push方法
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)
}
}
由上所知,beforeMount是在mountComponent中执行,所以是先父组件mountComponent先执行。而mounted则不同,子组件的mount是在执行insert hook的时候才执行。
子组件和父组件执行insert的顺序是怎样的?这将影响到子组件和父组件的mounted 钩子函数的执行顺序。
在createComponent 这个函数中我们可以看到,当发现是组件的时候就会执行init hook
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
init又会开始创建子组件内容,直到创建完,子组件开始执行initComponent和insert
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm)
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
initComponent就是把组件插入到insertedVnodeQueue
insert 就是把组件插入到父节点的DOM中
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
}
}
}
由此debug图可见,当我们创建真正DOM元素的时候,会先判断当前节点是不是组件,然后如果是组件则重新init然后走child.$mount(),如果不是组件则会判断是否有children,如果有则走createChildren,让我们的递归得循环下去。
而且当我们循环下去的时候,在createChildren后面才执行insert插入节点操作,那么意味则,只要children的循环创建没结束那么最外层的vnode就不会插入到DOM中,所以insert操作是子组件先insert然后才到父组件一层层往外扩散而insertedVnodeQueue同理也是先push子节点再push父节点。
insertedVnodeQueue的执行上面说到是invokeInsertHook,因为遍历是从0开始,所以我们先执行的就是子组件的insert hook。由此可见组件的mounted是从子组件开始执行的
beforeDestroy&destroyed
beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 $destroy 方法
beforeDestroy 钩子函数的执行时机是在 $destroy 函数执行最开始的地方,接着执行了一系列的销毁动作,包括从 parent 的 $children 中删掉自身,删除 watcher,当前渲染的 VNode 执行销毁钩子函数等,执行完毕后再调用 destroy 钩子函数。
在 $destroy 的执行过程中,它又会执行 vm.patch(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用,所以 destroy 钩子函数执行顺序是先子后父,和 mounted 过程一样。