Vue组件的生命周期

Vue的组件从创建、到数据更新最后到组件销毁,在这个过程中Vue框架都会对组件都会经过一系列固定的操作,例如初始化监听器、更新DOM等等。

在这个过程中,Vue也提供了不同阶段的回调函数给开发者,来执行自己的业务逻辑。 有关Vue组件生命周期的知识在官方文档中已经介绍的比较详细了,这里进行一些总结和梳理,中间将相关知识点串联起来。

组件的创建与初始化

创建阶段在Vue实例的生命周期中只会执行一次,主要进行Vue组件实例的初始化的相关工作。

...
// 初始化Vue实例
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')

在组件的创建阶段,有beforeCreatecreated两个钩子函数提供给开发者执行相关操作, 我们也按钩子函数划分阶段进行讨论。

I. ——> beforeCreate

beforeCreate方法回调之前,Vue组件将进行一些初始化操作, 主要包括以下几个部分:

  1. 收集并初始化组件options
// 初始化options
// 缓存实例对象,便于操作
const vm: Component = this
// a uid for vue instance
vm._uid = uid++

// 标志位,避免Vue实例对象本身被声明为响应式数据
vm._isVue = true
// merge options
...
vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),// 解析出对象中定义的options
  options || {},
  vm
)
// dev模式下,hook掉vue实例上属性的handler操作,便于开发check
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

options对象承载了开发者对于组件的输入和定义,其中又各式各样的语法糖格式, mergeOptions不仅对options进行了合并,同时也包括属性等option的泛化处理,保证格式统一。

  1. 初始化Vue实例属性
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
}

这里在初始化属性值时,也建立了Vue父组件与子组件的联系,构建整个组件实例的树结构。

  1. 初始化Vue实例的事件

我们知道,子组件通过向父组件emit事件来完成通信,这里先找到父组件中所监听的事件:

// 找到父组件中所监听的事件
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)
  }
}

然后告诉子组件,父组件都监听了哪些事件:

function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

updateListeners方法中遍历了父组件中的事件监听器, 并通过调用vm.$on方法注册到子组件的监听器中去。

Vue组件维护了一个监听器对象,来存储组件所监听的事件

  1. 初始化渲染相关属性和上下文

至此,Vue组件的实例对象完成了一些内部属性相关的初始化工作,但并不包括组件内部的响应式数据部分,因此我们在beforeCreate方法中是无法访问到组件状态和方法相关数据。

II. beforeCreate ——> created

而在beforeCreate回调之后,则会对数据进行响应式化的处理, 主要包括以下两个方面:

  1. 初始化provide / inject
  2. 初始化组件的数据状态

组件状态的相关数据主要包括父组件传递过来的props,以及自身的data,method,watcher等等。 这部分工作主要包括两个部分,一个是相关数据的校验以及初始化,另一部分是Vue的响应式系统会对组件状态数据的收集,从而监听数据变化,进行响应式更新操作。 具体内容见「Vue 响应式系统的原理」

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)
  }
}

这里进行了组件内部数据(data,props)的观测以及侦听器和计算属性的配置操作。 有关组件属性的使用和相关分析的具体内容见「Vue 属性」


至此,Vue组件实例对象的创建和初始化工作已经基本上完成了, 但此时组件还没有被挂载到DOM上。

组件的挂载

在Vue组件初始化工作基本完成后,则会进行组件的挂载工作,这里会有两种挂载方式:

  • 手动挂载:指定挂载节点,并调用$mount函数
  • 自动挂载:根据vm.$options.el所指定的挂载节点执行挂载

初始化末尾会判断该Vue组件是否指定了挂载节点:

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

如果Vue组件的options.el中指定了该组件的挂载节点,则主动调用$mount方法执行挂载相关逻辑; 否则该组件的生命周期暂停下来,处于一个“未挂载”的状态,直到vm.$mount方法被调用。

模板的编译( created ——> beforeMount )

我们知道,Vue提供了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。

Vue的编译器会对通过模板文件来定义的组件进行编译,生成一个可供调用的render函数。

如果我们没有为Vue实例指定模板(options.template),Vue则会选择以挂载节点el外层的html作为模板。

编译模板为render函数这部分工作,会根据使用的Vue包不同而有所区别($mount函数不同): 如果使用了运行时版本,则需要在代码构建过程中完成模板的编译;而如果采用了完整版,则模板的编译则放在组件的挂载之前。

另外,根据指定的el,查找到组件的挂载点:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ......
  callHook(vm, 'beforeMount')
}

可以看到在beforeMount方法被回调执行之前,Vue组件此时已经完成了模板编译以及挂载点查找。

beforeMount ——> mounted

我们都知道,为了避免直接操作原始DOM树,Vue采用了Virtual Dom机制来完成相关的计算, 而前面经过编辑的模板所生成了render函数,它作用就是返回了对应组件的VNode节点。

在前面的mountComponent方法中创建了一个监听器来追踪数据的变化,并执行渲染和更新操作:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
}

当数据发生变化则会回调updateComponent方法,触发渲染和更新操作,并将真实DOM渲染在页面上。


至此, 组件的渲染和挂载执行完毕,并展示在页面上。

组件销毁

通常情况下,路由跳转等操作都会触发组件的销毁操作。

beforeDestroy ——> destroyed

beforeDestroy方法会在组件刚刚开始销毁时回调,可以进行定时器销毁等操作。

随后的工作包括以下几个部分:

  1. 从父组件中移除
 callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
  remove(parent.$children, vm)
}
  1. 移除监听器
// teardown watchers
if (vm._watcher) {
  vm._watcher.teardown()
}
vm.$off()
  1. 销毁实例相关属性和引用
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
  vm._data.__ob__.vmCount--
}