深入学习vue系列 —— 生命周期钩子

122 阅读1分钟

讲到生命周期必须要来一张图片:

vue有8种生命周期函数:

先看看callHook函数作用:调用生命周期钩子函数

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  //为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
  pushTarget()
  //获取生命周期钩子 vue选项合并会把生命周期钩子选项合并成一个数组
  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)
  }
  //为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
  popTarget()
}

vm._hasHookEvent 是在 initEvents 函数中定义的,它的作用是判断是否存在生命周期钩子的事件侦听器,初始化值为 false 代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将 vm._hasHookEvent 设置为 true

生命周期钩子数组遍历时调用 invokeWithErrorHandling 函数,实际上执行的就是 handler.call(context)

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
}

下面针对每一个生命周期钩子函数进行分析

callHook(vm, 'beforeCreate')

initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')

在执行 _init() 方法时开始初始化了生命周期、事件和渲染。紧接着就调用了 beforeCreate 钩子函数。此时与数据相关的属性都还没有初始化 ,所以在这个阶段想要用获取到组件的属性是无法成功的。

callHook(vm, 'created')

initInjections(vm) // resolve injections before data/props
//其中 initState 包括了:initProps、initMethods、initData、initComputed 以及 initWatch
initState(vm) // 
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

在调用了 beforeCreate 钩子之后,Vue 调用了 initInjections(vm) 、initState(vm) 、 initProvide(vm) 这三个方法用于初始化 data 、props 、watcher 等等,在这些初始化执行完成之后,调用了 created 钩子函数,在这个时候,我们已经可以获取到 data、props 等数据了,但是 Vue 并没有开始渲染 DOM,所以我们还不能够访问 DOM(PS:我们可以通过vm.$nextTick来访问)。这一步之后,就开始进入渲染流程。

callHook(vm, 'beforeMount')

在调用了created钩子之后,Vue开始进行DOM的挂载,执行 $mount 方法的时候,开始装载组件,具体内容在 mountComponent 函数中,在此函数的最开始时渲染虚拟节点之前就调用了 beforeMount 钩子,然后开始执行 updateComponent 来渲染组件视图。

简单的来说这个时候在进行template模板的解析

callHook(vm, 'mounted')

相关的 render 函数首次被调用,调用完成之后,mounted 钩子被调用,标记着el 被新创建的 vm.$el 替换,并被挂载到实例上。这之后就开始处于生命周期的正常运转期,进入数据更新并重新渲染视图的循环中。

callHook(vm, 'beforeUpdate')

如果有数据的更新时就会先调用 beforeUpdate 钩子。

callHook(vm, 'updated')

当数据更新并且完成视图渲染后调用 updated 钩子。这个钩子和上面的钩子会一直在生命周期运转期里不断被触发。

callHook(vm, 'beforeDestroy') 和 callHook(vm, 'destroyed')

在执行销毁操作之前调用了callHook(vm, 'beforeDestroy'),然后执行了一系列的销毁操作,包括删除掉所有的自身(self)、_watcher、数据引用等等,删除完成之后调用callHook(vm, 'destroyed')。

源码如下:

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

至此 vue 的整个生命周期就已经结束了。但是还有在keep-alive情况下的两个钩子函数,下面简要描述一下。

callHook(vm, 'activated') 和 callHook(vm, 'deactivated')

activated、deactivated这两个钩子函数分别是在keep-alive 组件激活和停用之后回调的,它们不牵扯到整个Vue的生命周期之中。使用 keep-alive 模式在切换到不同组件视图的过程中不会进行重新加载,这就意味着其他的钩子函数都不会被调用,如果在离开页面和进入页面的时候执行某些操作,这两个钩子就非常有用。

源码如下:

export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}