Vue生命周期总结

这里主要记录在平常中对知识的学习,经过结合笔记与自身理解的方式尝试写下总结
文章对细节可能不会一一介绍解释,内容仅做参考
复制代码

这些天在尝试开始对Vue源码的解读,一点一点去了解框架的设计以及实现思路。今天在编码时候想了有关生命周期的问题,恰好晚上就看到了相关知识。做为其中一小步记录一下javascript

1、生命周期

每一个Vue实例在被建立以前都要通过一系列的初始化过程。例如设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等
在这个过程当中会执行相应的生命周期钩子函数,给予用户机会在一些特定的场景下添加本身的逻辑代码vue

直接贴上官方生命周期图:java

能够看出生命周期是描述了一个Vue实例在建立、挂载、注销、更新的一个流程

2、生命周期钩子的调用

在源码中最终执行生命周期的函数是callHook方法和invokeWithErrorHandling方法,它的定义在src/core/instance/lifecycle和src/core/util/error中能够看到:node

// src/core/instance/lifecycle
export function callHook (vm: Component, hook: string) {
  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()
}

// src/core/util/error
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)`))
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}
复制代码

callHook接收的两个参数分别为Vue实例和要触发的生命周期钩子名后端

在触发时api

  • 根据hook拿到对应的回调函数数组(vue实例在初始化时候,其中有个过程是合并options,在该操做时会收集各个阶段的生命周期钩子函数构成对应的数组,而后将他们都挂载到实例的options中(即vm.$options)。因此在这里拿到的是一个数组。具体的能够去看一下代码)
  • 若是数组有值,遍历代入invokeWithErrorHandling方法中执行(在方法中咱们能够看到使用了apply/call将实例vm做为函数执行上下文传入,这也是咱们在编写生命周期回调的时候,不能使用箭头函数的缘由:箭头函数的执行上下文指向定义该函数时的上下文,且没法改变,从而获取不到实例对象指向)

3、钩子罗列

查看Vue官网,很容易能够获得有以下钩子:数组

  1. beforeCreate
  2. created
  3. beforeMount
  4. mounted
  5. beforeUpdate
  6. updated
  7. activated
  8. deactivated
  9. beforeDestroy
  10. destroyed
  11. errorCaptured

至于他们都有什么做用,官网已经写得很详细,建议看一下:cn.vuejs.org/v2/api/app

除七、八、11外,其余的在平时开发中较经常使用到。它们的执行顺序跟排列顺序同样框架

4、beforeCreate和created

beforeCreate和created函数都是在实例化Vue的阶段,在_init方法中执行的,它的定义在src/core/instance/init中:dom

Vue.prototype._init = function (options?: Object) {
    ...
    vm._self = vm
    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')
    ...
复制代码

能够看到beforeCreate和created的钩子调用是在initState函数的先后,initState的做用是初始化props、data、methods、watch、computed等属性

那么显然beforeCreate的钩子函数中就不能获取到props、data中定义的值,也不能调用methods中定义的函数,而created能够
在这俩个钩子函数执行的时候,尚未渲染 DOM,所均访问不到DOM

通常来讲,若是组件在加载的时候须要和后端有交互,放在这俩个钩子函数执行均可以,若是是须要访问props、data等数据的话,就须要使用created钩子函数

5、beforeMount和mounted

beforeMount和mounted函数执行在Vue实例挂载阶段,它们的调用时机是在mountComponent函数中,定义在src/core/instance/lifecycle:

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

  ...

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

在执行vm._render()函数渲染 VNode 以前,执行了beforeMount钩子函数,在执行完vm._update()把 VNode patch 到真实 DOM 后,执行mounted钩子

注意,这里对mounted钩子函数执行有一个判断逻辑,vm.$vnode若是为null,则代表这不是一次组件的初始化过程,而是咱们经过外部new Vue初始化过程

而那么对于组件,它的mounted时机在哪儿呢
经过阅读源码咱们能够发现(伪装你们都阅读源码,由于上下文只对生命周期进行总结,因此深刻的就不说啦~),在组件VNode patch到DOM后,会执行invokeInsertHook函数(定义在src/core/vdom/patch.js),会把insertedVnodeQueue里面保存的全部mounted钩子函数依次执行一遍

这一些都是题外话了,先记住Vue组件在实例化的时候会先等待子组件的实例化完成,因此insertedVnodeQueue(保存组件的mounted钩子函数的数组)的添加顺序是先子后父

因此对于同步渲染的组件而言,mounted钩子函数的执行顺序是先子后父

6、beforeUpdate和updated

beforeUpdate和updated的钩子函数执行时机都是在数据更新的时候
beforeUpdate的执行时机是在 渲染Watcher 的before函数中,在mountComponent函数中能够看到(src/core/instance/lifecycle):

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

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

这里有个判断,也就是在组件已经mounted以后才会去调用这个钩子函数

update的执行时机是在flushSchedulerQueue函数调用的时候, 它的定义在src/core/observer/scheduler中:

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  ...
  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')
    }
  }
}
复制代码

至于深刻,这里就不解释啦~ (还不会

7、beforeDestroy和destroyed

beforeDestroy和destroyed钩子函数的执行时机在组件销毁的阶段
beforeDestroy钩子函数的执行时机是在destroy函数执行最开始的地方,接着执行了一系列的销毁动做,包括从parent的children中删掉自身,删除watcher,当前渲染的VNode执行销毁钩子函数等,执行完毕后再调用destroy钩子函数 在$destroy的执行过程当中,它又会执行vm.patch(vm._vnode, null)触发它子组件的销毁钩子函数,这样一层层的递归调用

因此destroy钩子函数执行顺序是先子后父,和mounted过程同样

8、activated和deactivated

activated是keep-alive组件激活时调用

deactivated是keep-alive组件停用时调用

9、总结

经过对整个生命周期的了解,就能够很清晰地知道能够在什么阶段作什么事,或者某一操做应该在什么阶段执行

例如在create中进行数据操做,在mounted中进行DOM完成后的操做,在destroyed进行事件解绑和功能注销

文章篇幅有点多,不少都是一些相关的衍生,只有在进行源码解读的时候才比较容易理解。在这里最重要的是知道每一个生命周期钩子的时机和做用,其余都是浮云~