生命周期的讲解可以参考这篇文章
上篇文章更偏向于解释vue生命周期在不同的阶段都做了什么,但是他又是如何实现的我们不得而知,vue生命周期主要来说分为四个阶段,初始化阶段、模板编译阶段、挂载阶段和销毁阶段,从源码的角度来深入学习一下vue生命周期初始化阶段所做的工作和内部原理。
初始化阶段
从生命周期流程图中我们可以看到,初始化阶段所做的工作也可大致分为两部分:第一部分是new Vue(),也就是创建一个Vue实例;第二部分是为创建好的Vue实例初始化一些事件、属性、响应式数据等。
new Vue()
new 关键字在 JS中表示从一个类中实例化出一个对象来,由此, Vue 实际上是一个类,所以new Vue()实际上是执行了Vue类的构造函数。
在源码的src/core/instance/index.js文件里看到了Vue类的定义
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) // 核心代码
}
我们可以看出在这段代码中的核心代码其实就是this._init(options),调用原型上的_init()方法并将option传入。而_init方法又是通过执行initMixin(Vue)得到的,接下来在src/core/instance/init.js文件下看看initMixin函数:
这是全部的initMixin方法的代码:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
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)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
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')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
为了更加直观,我们把一些不太重要的代码剔除然后留下核心代码:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._isVue = true
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 方法,同时 _init 方法的定义也在该函数内部。现在我们知道了,new Vue() 会执行 Vue 类的构造函数,构造函数内部会执行 _init 方法,所以 new Vue() 的作用其实就是 _init 方法的作用,那么我们仔细看看_init 方法都干了哪些事情:
- 首先,把
Vue实例赋值给变量vm,并且把用户传递的options选项与当前构造函数的options属性及其父级构造函数的options属性进行合并(关于属性如何合并的问题下面会介绍),得到一个新的options选项赋值给$options属性,并将$options属性挂载到Vue实例上。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor)
options || {}
vm
)
- 然后通过调用一些初始化函数来为
Vue实例初始化一些属性,事件,响应式数据。
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) //初始化injections
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
- 在所有的初始化工作完成后,判断是否传入了
el选项,如果传入了则调用$mount函数进入模板编译和挂载阶段,如果没有就不进入下一个生命周期需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
由上面的的new Vue()阶段可以看出它完成了 Vue 的整个初始化阶段,接下来再来看看 new Vue() 里调用的那几个初始化函数。
属性合并
_init 方法里首先会调用 mergeOptions 函数来进行属性合并。
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor)
options || {}
vm
)
它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,下面就是resolveConstructorOptions()方法:
function resolveConstructorOptions (Ctor) {
var options = Ctor.options;
if (Ctor.super) {
var superOptions = resolveConstructorOptions(Ctor.super);
var cachedSuperOptions = Ctor.superOptions;
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions;
// check if there are any late-modified/attached options (#4976)
var modifiedOptions = resolveModifiedOptions(Ctor);
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions);
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
if (options.name) {
options.components[options.name] = Ctor;
}
}
}
return options
}
在 initGlobalAPI(Vue) 的时候定义了 Vue.options,代码在 src/core/global-api/index.js 中:
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
首先通过 Vue.options = Object.create(null) 创建一个空对象,然后遍历 ASSET_TYPES,ASSET_TYPES 的定义在 src/shared/constants.js 中:
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
遍历 ASSET_TYPES 后的代码相当于:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
最后通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前 有<keep-alive>、<transition> 和<transition-group> 组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。
那么回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 中,可以看出,mergeOptions函数的主要功能是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。
/**
* 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 (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) { // 递归把 `extends` 和 `mixins` 合并到 `parent` 上
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {} // 创建一个空对象options,遍历 parent,把 parent 中的每一项通过调用 mergeField 函数合并到空对象options里,
let key
for (key in parent) {
mergeField(key)
}
for (key in child) { // 接着再遍历 child,把存在于 child 里但又不在 parent 中的属性继续调用 mergeField 函数合并到空对象options里
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 // options就是最终合并后得到的结果
}
首先递归把 extends 和 mixins 合并到 parent 上,然后创建一个空对象options,遍历 parent,把parent中的每一项通过调用 mergeField函数合并到空对象options里,接着再遍历 child,把存在于child里但又不在 parent中 的属性继续调用 mergeField函数合并到空对象options里,最后,options就是最终合并后得到的结果,将其返回。
这里值得一提的是 mergeField 函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。例如,对于data有data的合并策略,即该文件中的strats.data函数;对于watch有watch的合并策略,即该文件中的strats.watch函数等等。这就是设计模式中非常典型的策略模式。
callHook函数触发钩子函数
函数的源码位于src/core/instance/lifecycle.js 中:
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()
}
首先从实例的$options中获取到需要触发的钩子名称所对应的钩子函数数组handlers,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。
注:文章部分摘自vue源码学习社区,原文十分详细,笔者只是跟随思路阅读并手打一遍留下印象,方便以后回顾,感兴趣的同学可到原文中细学。