# 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)
}
1
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)
  }
}
1
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
)
1
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')
1
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)
}
1
2
3

这里会判断用户是否传入了 el 选项,如果传入则调用 $mount 函数进行挂载操作,否则就需要用户手动调用 $mount 。以上就是 new Vue() 所做的全部事情,此时生命周期就变为了 mounted。从上面的代码可知:Vue 实例在自身生命周期中只会经历一次 beforeCreatecreatedbeforeMountmounted

# 合并属性

_init 函数中合并属性时,首先会调用 resolveConstructorOptions(vm.constructor) 得到一个返回值,然后调用 mergeOtions 函数将返回值与用户传入的 options 进行合并操作。下面具体先看看 resolveConstructorOptions 函数返回了什么:

// src/core/instance/init.js
export function resolveConstructorOptions(Ctor) {
  let options = Ctor.options;
  // 这里不关心具体细节,省略部分代码

  return options;
}
1
2
3
4
5
6
7

可见,resolveConstructorOptions 函数返回的是 Vue 构造器上面的 options 的值。该 optionssrc/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);
}
1
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"
];
1
2
3
4
5

遍历之后调用 extend(Vue.options.components, buildInComponents),将内置组件 <keep-alive> 扩展至 components 对象中。最终得到的对象:

Vue.options = {
  components: {
    KeepAlive: {
      ...相关属性
    }
  },
  directives: {},
  filters: {}
}
1
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

参数 parent 是上面 Vue.optionschild 是用户传入的 options。首先遍历 parent 中的属性,通过 mergeField 方法进行合并操作,其次遍历 child 中的属性,再通过 mergeField 方法进行合并操作,下面我们着重看一下 mergeField 方法:

function mergeField (key) {
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}
1
2
3
4

mergerField 中将 parentchild 中的属性通过合并策略合并后赋值到新对象 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
}
1
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)
}
1
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
      }
    }
  }
}
1
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: ƒ,}
}
1
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
}
1
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
1
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)
  }
}
1
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)
}
1
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)
  }
}
1
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
})
1
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
}
1
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
}
1
2
3
4
5
6
7
8
9

这里主要是对合并后的生命周期数组进行去重操作。为什么合并后是数组?为什么要对数组去重?

# 初始化阶段(initInjections)

调用钩子函数之后,下一步就要初始化注入 initInjections(vm)

# 初始化阶段(initState)

该阶段会对 propmethodsdatacomputedwatch 这些属性进行一系列初始化,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)
  }
}
1
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 */)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

以上代码中省略了 Vuedata 的校验。这里的代码逻辑很简单,首先在 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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

使用数据劫持的方式将 _data 中的每个属性添加到 target(Vue 实例)上,并且每个属性都定义了一个 gettersetter。当在实例中使用 this.xxx 的时候,会触发属性的 getter,即执行 return this[sourceKey][key],这里的 this 指向 Vue 实例,sourceKey_datakey 是我们想访问的属性名,转换一下就是 return this["_data"][xxx] 。给某个属性赋值的时候同理。代理完成后,开始将数据设置为响应式,这也是 Vue 的核心。

# 调用 created 钩子函数

最终调用 callHook(vm, 'created'),完成初始化阶段。

# 编译模板阶段

vm.$mount(vm.$options.el) 开启模板编译阶段。