组件生命周期

在介绍完组件patch章节后,从new Vue实例化到最终渲染成真实DOM到视图的主线过程我们已经介绍完毕了,那么我们回顾一下这个过程,再看组件生命周期,在Vue.js官网中有这样一张组件生命周期流程图。

组件生命周期

callhook

在介绍生命周期函数之前,我们先来看一下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()
}

代码分析:

  • 我们可以看到在for遍历之前,使用了pushTarget,在遍历之后使用了popTargetpushTargetpopTarget在之前的章节中我们介绍过,这里主要提一个issue 7573open in new window,你在这个issue上面可以看到为什么要添加这两段代码。
  • 通过在this.$options对象上拿到hook参数对应callback数组,然后使用for循环遍历,在每个循环中通过invokeWithErrorHandling来触发回调函数。invokeWithErrorHandling方法是定义在src/core/util/error.js文件中的一个方法,其代码如下:
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

我们可以看到invokeWithErrorHandling方法的代码不是很多,核心就是下面这段代码,其它属于异常处理。

res = args ? handler.apply(context, args) : handler.call(context)
  • for循环遍历之后,它判断了vm._hasHookEvent,你可能会很好奇这个内部属性在哪里定义的?是做什么的?在initEvents方法中,首先默认设置这个属性为false,代码如下:
export function initEvents (vm: Component) {
  // ...
  vm._hasHookEvent = false
  // ...
}

在事件中心$on方法中,它根据正则条件判断,如果判断为真则赋值为true,代码如下:

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    // optimize hook:event cost by using a boolean flag marked at registration
    // instead of a hash lookup
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}

_hasHookEvent属性为真,组件会触发对应的生命周期钩子函数,那么我们可以利用这个功能做两件事情:监听子组件生命周期监听组件自身生命周期

假设我们有如下组件:

<template>
  <div id="app">
    <hello-world @hook:created="handleChildCreated" :msg="msg" />
  </div>
</template>
<script>
export default {
  name: 'App',
  data () {
    return {
      msg: 'message'
    }
  },
  methods: {
    handleChildCreated () {
      console.log('child created hook callback')
    }
  },
  created () {
    const listenResize = () => {
      console.log('window resize callback')
    }
    window.addEventListener('resize', listenResize)
    this.$on('hook:destroyed', () => {
      window.removeEventListener('resize', listenResize)
    })
  }
}
</script>

代码分析:

  • template模板中,我们可以使用@hook:xxx的形式来监听子组件对应的生命周期,当对应的生命周期函数被触发的时候,会执行我们提供的回调函数,这种做法对于需要监听子组件某个生命周期的需求来说十分有用。
  • 在撰写Vue应用的时候,我们经常需要在created/mounted等生命周期中监听resize/scroll等事件,然后在beforeDestroy/destroyed生命周期中移除。对于这种需求,我们可以把逻辑写在同一个地方,而不是分散在两个生命周期中,这对于需要监听自身生命周期的需要来说也十分有用。

生命周期

beforeCreate和created

我们先来看beforeCreatecreated这一对钩子函数,它们是在this._init方法中被触发的:

Vue.prototype._init = function () {
  // ...
  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生命周期中间,它调用了三个方法,这几个方法是用来初始化injectdatapropsmethodscomputedwatch以及provide等这些配置选项的。那么我们可以得出一个结论,以上这些属性我们只有在created中才可以访问到,在beforeCreate中访问不到,因为还没有初始化。

beforeMount和mounted

在前面介绍$mount方法的时候,我们提到过beforeMountmounted这两个方法,它们是在mountComponent中被触发的,代码如下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // ...
  callHook(vm, 'beforeMount')
  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // ...
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

我们可以看到,在mountComponent方法的最前面,它首先调用了beforeMount方法,然后开始执行vm._update(),这个方法在组件首次渲染和派发更新时递归渲染父子组件的时候被调用。

在渲染完毕后,它判断了vm.$vode == null,如果条件满足才会触发mounted方法。你可能会很奇怪为什么这样做?在之前介绍update/path章节的时候,我们提到过一对父子关系:vm._vnodevm.$vnode,其中vm.$vnode表示父级的vnode。那么什么时候vm.$vnode会为null呢?答案是只有根实例,因为只有根实例才会满足这个条件,也就是说这里触发的是根实例的mounted方法,而不是组件的mounted方法。

根据beforeMountmounted的调用时机,我们可以知道:beforeMount生命周期是在vm._update()之前调用的,因此在这个生命周期的时候,我们还无法获取到正确的DOM。而mounted生命周期是在vm._update()方法之后执行的,所以我们可以在这个生命周期获取到正确的DOM

patch的时候,我们提到过VNode有一些钩子函数,我们来回顾一下:

const componentVNodeHooks = {
  init: function () {},
  prepatch: function () {},
  insert: function (vnode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    // ...
  },
  destroy: function () {}
}

其中,在insert钩子函数被触发的时候,它也触发了其组件的mounted方法,因此组件的mounted生命周期是在VNode触发insert钩子函数的时候被调用的。

beforeUpdate和updated

beforeUpdateupdated这一对生命周期钩子函数,是在派发更新的过程中被触发的。我们回顾一下依赖收集/派发更新这两个小节的内容,当某个响应式变量值更新的时候,会触发setter

Object.defineProperty(obj, key {
  set: function reactiveSetter (newVal) {
    // ...
    dep.notify()
  }
})

setter中会调用dep.notify()方法,去通知观察者更新,在notify实现方法中,它遍历了其subs数组,然后依次调用update()方法。

export default class Dep {
  // ...
  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这些Watcher实例的update最后会走到flushSchedulerQueue方法,在这个方法中会调用一个callUpdatedHooks方法

function flushSchedulerQueue () {
  // ...
  callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

callUpdatedHooks这个方法里面,它会遍历queueWatcher实例队列,在每个遍历的过程中,会触发vmupdated方法。当updated钩子函数被触发后,就代表派发更新阶段已经完成。

以上是对updated钩子函数的介绍,那么beforeUpdate呢,其实它是在实例化render watcher的时候被处理的。

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

我们可以看到,在实例化render watcher的时候,它给第四个参数传对象递了一个before属性,这个属性会被赋值到Watcher实例的before属性上。然后在flushSchedulerQueue方法遍历queue队列的时候,它首先判断了watcher.before是否存在,存在则调用这这个方法。

function flushSchedulerQueue () {
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    // ...
  }
  // ...
  callUpdatedHooks(updatedQueue)
}

beforeDestroy和destroyed

无论是beforeDestroy还是destroyed生命周期,都是在vm.$destroy实例方法中被触发的,这个方法它是在lifecycleMixin中被定义的,其代码如下:

export function lifecycleMixin (Vue) {
  // ..
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    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)
    }
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    vm.$off()
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

我们可以看到,在$destroy方法的最开始,它首先触发了beforeDestroy生命周期,随后又处理了一些其它操作:在父组件的$children移除自身移除自身依赖触发子组件销毁动作以及移除事件监听等。

接下来,我们以上面这几个步骤来说明:

  • 在父组件的$children移除自身:当某个组件销毁的时候,我们需要从其父组件的$children列表中移除自身,以下面代码为例:
<template>
  <div class="parent">
    <child-component />
  </div>
</template>

ChildComponent组件销毁之前,ParentComponent组件的$children数组保存了其引用关系,当ChildComponent销毁的时候,为了正确保持这种引用关系,我们需要从$children列表中移除。

// 展示使用,实际为vm实例
// 移除前
const $children = ['child-component', ...]

// 移除后
const $children = [...]
  • 移除自身依赖:在之前,我们提到过vm._watchers维护了一份观察者数组,它们都是Watcher实例,另外一个vm._watcher指的是当前组件的render watcher。当组件销毁的时候,需要把这些观察者移除掉,它们都通过Watcher实例的teardown方法来实现,其代码如下:
export default class Watcher {
  // ...
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}
  • 触发子组件销毁动作:在移除Watcher以后,它随后调用了vm.__patch__方法,我们在之前update/patch章节介绍过这个方法,这里注意它第二个参数传递了null,我们回顾一下patch方法的实现:
export function createPatchFunction (backend) {
  // ...
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    // ...
  }
}

patch方法中,当我们传递的第二个参数vnodenull的时候,它会调用invokeDestroyHook方法,这个方法的代码如下:

function invokeDestroyHook (vnode) {
  let i, j
  const data = vnode.data
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  }
  if (isDef(i = vnode.children)) {
    for (j = 0; j < vnode.children.length; ++j) {
      invokeDestroyHook(vnode.children[j])
    }
  }
}

这个方法的主要作用就是递归调用子组件VNodedestroy钩子函数,我们来看一下VNode钩子函数destroy具体做了哪些事情:

const componentVNodeHooks = {
  // ...
  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}

我们可以看到,在destroy钩子函数中,如果忽略keep-alive相关的逻辑,它的核心还是调用组件的$destroy()方法。

小结:组件销毁的过程,应该是从父组件开始,然后递归销毁子组件,当子组件都销毁完毕时,父组件基本完成了销毁动作。因此父子组件关于beforeDestroydestroyed这两个生命周期钩子函数的执行顺序为:

// parent beforeDestroy
// child beforeDestroy
// child destroyed
// parent destroyed
  • 移除事件监听:在前面我们提到当子组件完成销毁动作时,父组件基本也完成了销毁动作。这是因为,在使用callHook触发destroyed生命周期钩子函数之后,我们还需要移除相关的事件监听,它使用$off来实现,我们回顾一下代码:
Vue.prototype.$off = function (
  event?: string | Array<string>,
  fn?: Function
): Component {
  const vm: Component = this
  // all
  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }
  // ...
  return vm
}

当我们不传递任何参数的时候,它直接把vm._events赋值为一个空对象,这样就达到了移除事件监听的目的。

activated和deactivated

这两个生命周期方法是与keep-alive内置组件强相关的生命周期钩子函数,因此我们会把这两个钩子函数的介绍放在之后的keep-alive小节。

最后更新时间:
贡献者: wangtunan