# Vue 的生命周期
每一个 Vue 实例都会经历初始化->挂载元素->销毁等一系列的生命周期,官网提供一张详细的生命周期示意图:
图中清晰地反映了生命周期中每个阶段 Vue 干了哪些事,下面结合 Vue 的源码,我们再详细学习一番生命周期。
# 初始化阶段(new Vue)
我们在使用 Vue 时,首先要创建一个实例,即 new Vue(options)
,options 是一个选项对象,包含了挂载根元素、数据、方法等等一系列控制页面交互的东西。下面我们看看构造函数都干了什么。
# 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)
}
2
3
4
5
6
7
8
9
核心代码就是 this._init(options)
这一行,调用原型上面的 _init
方法并将用户写的选项对象 options 作为参数传入。
# 原型方法 _init
_init 方法定义在 Vue 原型上,下面省略部分源码,只列出关键性的代码
// src/core/instance/init.js
Vue.prototype._init = function (options) {
const vm = this
vm._uid = uid++
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)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
可以看到,首先将 Vue 实例赋值给变量 vm,然后将用户传入的 options 与内置的 options 对象合并为一个新对象(下面会介绍选项合并的问题),并赋值给实例属性 $options,如下:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
2
3
4
5
接着,进行一系列的初始化操作:初始化生命周期、初始化事件、初始化渲染等等,并且会在恰当的时机调用不同的生命周期钩子函数 callHook(vm, xxx)
,如下:
// 初始化生命周期
initLifecycle(vm)
// 初始化事件
initEvents(vm)
// 初始化渲染
initRender(vm)
// 调用 beforeCreate 钩子函数
callHook(vm, 'beforeCreate')
// 初始化注入
initInjections(vm)
// 初始化 props、methods、data、computed、watch
initState(vm)
// 初始化 provide
initProvide(vm)
// 调用 created 钩子函数
callHook(vm, 'created')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
完成了上面一系列初始化操作后,Vue 实例就需要进行挂载操作了,如下:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
2
3
这里会判断用户是否传入了 el
选项,如果传入则调用 $mount
函数进行挂载操作,否则就需要用户手动调用 $mount
。以上就是 new Vue()
所做的全部事情,此时生命周期就变为了 mounted。从上面的代码可知:Vue 实例在自身生命周期中只会经历一次 beforeCreate、created、beforeMount 和 mounted。
# 合并属性
在 _init
函数中合并属性时,首先会调用 resolveConstructorOptions(vm.constructor)
得到一个返回值,然后调用 mergeOtions
函数将返回值与用户传入的 options 进行合并操作。下面具体先看看 resolveConstructorOptions 函数返回了什么:
// src/core/instance/init.js
export function resolveConstructorOptions(Ctor) {
let options = Ctor.options;
// 这里不关心具体细节,省略部分代码
return options;
}
2
3
4
5
6
7
可见,resolveConstructorOptions 函数返回的是 Vue 构造器上面的 options 的值。该 options 在 src/core/global-api
中被定义:
export function initGlobalApi(Vue) {
Vue.options = Object.create(null);
ASSET_TYPES.forEach(type => {
Vue.options[type + "s"] = Object.create(null);
});
Vue.options._base = Vue;
// 扩展内置组件 <keep-alive>
extend(Vue.options.components, buildInComponents);
}
2
3
4
5
6
7
8
9
10
首先通过 Object.create(null)
创建了一个空对象,然后遍历 ASSET_TYPES,其定义在 src/shared/constants.js
:
export const ASSET_TYPES = [
"component",
"directive",
"filter"
];
2
3
4
5
遍历之后调用 extend(Vue.options.components, buildInComponents)
,将内置组件 <keep-alive>
扩展至 components 对象中。最终得到的对象:
Vue.options = {
components: {
KeepAlive: {
...相关属性
}
},
directives: {},
filters: {}
}
2
3
4
5
6
7
8
9
回到 mergeOptions 这个函数,它定义在 src/core/util/options.js
中:
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 省略部分代码
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
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
参数 parent
是上面 Vue.options
,child
是用户传入的 options
。首先遍历 parent
中的属性,通过 mergeField
方法进行合并操作,其次遍历 child
中的属性,再通过 mergeField
方法进行合并操作,下面我们着重看一下 mergeField
方法:
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
2
3
4
mergerField
中将 parent
和 child
中的属性通过合并策略合并后赋值到新对象 options
中。注意,这里使用了 策略模式 来合并每一种属性。例如:data
属性会根据 data
的合并策略来合并。方法中第一行根据属性 key
来获取对应的合并策略, strats
对象其实就是(options.js 中可以找到如何构建该对象):
strats = {
data: fn,
beforeCreate: fn,
created: fn,
beforeMount: fn,
mounted: fn,
beforeUpdate: fn,
updated: fn,
beforeDestroy: fn,
destroyed: fn,
components: fn,
directives: fn,
filters: fn,
watch: fn,
computed: fn,
inject: fn,
methods: fn,
props: fn,
provide: fn
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可见,每个属性对应的合并策略都是函数,然后调用策略函数完成属性的合并。下面,以 data
属性为例,data
对应的策略函数:
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
代码中首先判断是否传入 vm
,若没有则继续判断 childVal
是否为函数,这里针对的是定义一个组件的情况,我们都知道定义组件时, data
属性需要写成一个返回对象的函数,这是为了保证每个组件实例中的 data
数据都是独立的。咱们这里传入了 vm
,那么执行 return mergeDataOrFn(parentVal, childVal, vm)
:
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
return function mergedInstanceDataFn () {
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
由于传入了 vm
,那么返回一个名为 mergedInstanceDataFn
的函数,也就是说合并之后的 data
属性其实是一个函数。思考:为什么不直接返回一个合并后的对象。
回到 _init
方法中,得到合并后的 options
之后,实例 vm
变为了:
vm = {
_uid: 0
_isVue: true
$options: {components: {…}, directives: {…}, filters: {…}, el: "#app", _base: ƒ, …}
}
2
3
4
5
# 初始化阶段(initLifecycle)
之后执行 initLifecycle(vm)
初始化生命周期,具体代码逻辑在 src/core/instance/lifecycle.js
:
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
initLifecycle
主要为实例对象挂载了诸多属性并且设置了默认值,其中最重要的是 $parent
和 $root
属性:
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
2
3
4
5
6
7
8
9
10
若当前组件存在父组件且当前组件不是抽象组件,进行 while 循环。在 Vue 中最终会形成一颗组件树,这里 while 就是向上寻找第一个不是抽象组件的父组件,然后将当前组件添加到父组件的 $children
中,并且把这个父组件添加到当前组件的 $parent
属性中。这样一来,父组件可以访问到子组件,子组件也可以访问到父组件。设置当前组件的根组件 $root
时,同样会向上寻找,若父组件存在,就设置为父组件的根组件,否则自身就是根组件。
# 初始化阶段(initEvents)
接下来,initEvents(vm)
初始化事件,具体代码位于src/core/instance/events.js
:
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
2
3
4
5
6
7
8
9
这里代码很简单,给 vm
实例挂载了 _events
和 _hasHookEvent
两个属性,并且若存在 _parentListeners
则调用 updateComponentListeners(vm, listeners)
,后续会详细分析 Vue 中的事件系统。
# 初始化阶段(initRender)
轮到初始化渲染 initRender(vm)
,具体代码位于src/core/observer/index.js
:
export function initRender (vm: Component) {
vm._vnode = null
vm._staticTrees = null
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
const parentData = parentVnode && parentVnode.data
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
与初始化生命周期类似,给 vm
实例挂载各种属性并且设置默认值,有趣的是,$attrs
和 $listeners
被定义成了响应式,在创建高阶组件的时候,这两个属性非常有用。
# 调用 beforeCreate 钩子函数
经历了上面一系列初始化之后,Vue 会调用第一个生命周期钩子函数 callHook(vm, 'beforeCreate')
,callHook
函数位于src/core/instance/lifecycle.js
:
export function callHook (vm: Component, hook: string) {
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)
}
}
2
3
4
5
6
7
8
9
10
11
12
代码逻辑很简单,找到对应生命周期钩子函数,然后调用 invokeWithErrorHandling
方法执行。从初始化事件分析中可知,此时 vm._hasHookEvent
属性为 false
,因此 if
语句内部不执行。没错,这里的钩子函数竟然是一个数组,为什么会是个数组呢?去看看生命周期钩子函数合并策略把。回到 src/core/util/options.js
,找到定义其策略的代码:
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})
2
3
LIFECYCLE_HOOKS
是一个常量,来自 src/shared/constants.js
,其值为生命周期字符串数组,即 ["beforeCreate","created","beforeMount",...]
。每一个生命周期的合并策略都是 mergeHook
:
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
return res
? dedupeHooks(res)
: res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
先回顾一下,parentVal
是指 resolveConstructorOptions(Vue.constructor)
返回的对象中的选项值,注意:是一个数组。childVal
是用户传入的选项值。 这里有一个很长的三元表达式:
- 判断
childVal
是否存在 - 存在,判断
parentVal
是否存在- 存在,将
childVal
合并到parentVal
中 - 不存在,判断
childVal
是否为数组- 是数组直接返回
- 非数组,放到数组中并返回此数组
- 存在,将
- 不存在,变量
res
赋值为parentVal
然后判断 res
是否存在,存在就调用 dedupeHooks
:
function dedupeHooks (hooks) {
const res = []
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i])
}
}
return res
}
2
3
4
5
6
7
8
9
这里主要是对合并后的生命周期数组进行去重操作。为什么合并后是数组?为什么要对数组去重?
# 初始化阶段(initInjections)
调用钩子函数之后,下一步就要初始化注入 initInjections(vm)
# 初始化阶段(initState)
该阶段会对 prop
、methods
、data
、computed
、watch
这些属性进行一系列初始化,initState
的代码位于 src/core/instance/state.js
:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 初始化 data
若选项对象存在 data
属性,调用 initData
进行初始化,否则使用一个空对象作为实例的 data
属性,并且设置为响应式。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
}
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以上代码中省略了 Vue 对 data
的校验。这里的代码逻辑很简单,首先在 Vue 实例上挂载了 _data
属性,并且赋值为 $options.data
值(注意:若 data
为函数,则需要调用 getData
获取真正的数据)。接着,遍历 data
对象中的所有属性(Object.keys() 只返回对象自身可枚举的属性
),依次在实例上设置代理。代理之后,我们在实例中就可以用 this.xxx
的方式直接访问 data
中的属性了。代理逻辑如下:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用数据劫持的方式将 _data
中的每个属性添加到 target
(Vue 实例)上,并且每个属性都定义了一个 getter 和 setter。当在实例中使用 this.xxx
的时候,会触发属性的 getter,即执行 return this[sourceKey][key]
,这里的 this
指向 Vue 实例,sourceKey
为 _data
,key
是我们想访问的属性名,转换一下就是 return this["_data"][xxx]
。给某个属性赋值的时候同理。代理完成后,开始将数据设置为响应式,这也是 Vue 的核心。
# 调用 created 钩子函数
最终调用 callHook(vm, 'created')
,完成初始化阶段。
# 编译模板阶段
vm.$mount(vm.$options.el)
开启模板编译阶段。