Vue的生命周期,就是在讲Vue从开始的创建,到最终的销毁,都经历了什么。
从这个图中可以看到,Vue从出生到死亡,一共要经历四个阶段:
- 初始化阶段:为
Vue
实例上初始化一些属性,事件以及响应式数据; - 模板编译阶段:将模板编译成渲染函数;
- 挂载阶段:将实例挂载到指定的
DOM
上,即将模板渲染到真实DOM
中; - 销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;
vue生命周期之初始化阶段(new Vue)
初始化阶段做的第一件事儿就是初始化实例,new Vue(),所以相当于创建了一个类,执行了构造函数。
// src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
其实很简单,就是调用了一个_init函数,接收了一个options参数。这个_init函数是从哪儿来的呢?
// src/core/instance/init.js
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
vm._self = vm
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')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
- 从上面的代码可以看到,initMixin函数只干了一件事儿,那么就是vue的原型上添加了一个_init的方法。所以这么说来,new Vue()实际上执行的是_init函数的内容。在_init的函数内部:
- 首先,将vue实例赋值给了vm,并且把用户传入的options,和当前构造函数的options,以及父级构造函数的options进行合并。将合并后的options赋值给$options,并且将这个$options挂载到vue实例上。
- 紧接着,就是通过调用一些初始化函数来为
Vue
实例初始化一些属性,事件,响应式数据等紧接着; - 最后,会判断用户有没有传入el选项,如果有,则调用
$mount
函数进入模板编译与挂载阶段,如果没有传入el
选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount
方法才进入下一个生命周期阶段。
下面来看看上面所说得options合并是如何完成的。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
可以看到,调用了mergeOptions方法,将resolveConstructorOptions()函数的返回值和options合并。resolveConstructorOptions就相当于Vue.options是在initGlobalAPI(Vue)中定义的值。
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
// ...
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
extend(Vue.options.components, builtInComponents)
// ...
}
// ASSET_TYPES 的定义在 src/shared/constants.js
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
Vue.options通过Object.create(null)创建了一个空对象,然后遍历ASSET_TYPES,生成了这样的代码:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
最后通过extend(Vue.options.components, builtInComponents)
把一些内置组件扩展到 Vue.options.components
上,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。
那么回到mergeOptions函数上,它是如何实现的呢?
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*/
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (typeof child === 'function') {
child = child.options
}
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
可以看出,mergeOptions
函数的 主要功能是把 parent
和 child
这两个对象根据一些合并策略,合并成一个新对象并返回。首先递归把 extends
和 mixins
合并到 parent
上。然后创建一个空对象options
,遍历 parent
,把parent
中的每一项通过调用 mergeField
函数合并到空对象options
里,接着再遍历 child
,把存在于child
里但又不在 parent
中 的属性继续调用 mergeField
函数合并到空对象options
里,最后,options
就是最终合并后得到的结果,将其返回。值得一提的是 mergeField
函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。
我们再来看下生命周期的合并策略:
/**
* Hooks and props are merged as arrays.
*/
function mergeHook (parentVal,childVal): {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
// src/shared/constants.js
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured'
]
通过多层的三目运算,最终将钩子函数转化成一个数组。Vue
允许用户使用Vue.mixin
方法(关于该方法会在后面章节中介绍)向实例混入自定义行为,Vue
的一些插件通常都是这么做的。所以当Vue.mixin
和用户在实例化Vue
时,如果设置了同一个钩子函数,那么在触发钩子函数时,就需要同时触发这个两个函数,所以转换成数组就是为了能在同一个生命周期钩子列表中保存多个钩子函数。
callHook
函数如何触发钩子函数?
export function callHook (vm: Component, hook: string) {
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm)
} catch (e) {
handleError(e, vm, `${hook} hook`)
}
}
}
}
可以看到,callHook
函数逻辑非常简单。首先从实例的$options
中获取到需要触发的钩子名称所对应的钩子函数数组handlers
,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。
未完,待续。。。