结合源码聊一聊Vue的生命周期

1,317 阅读4分钟
生命周期的是我们在开发中不可回避的话题。了解生命周期也可以让我们知道什么阶段可以做什么,以及更好的解决项目中遇到的问题。
本文包含个人理解内容,希望大家批判性阅读,如有问题欢迎交流~


文章说明

  1. 每一个 · 后跟的生命周期钩子可以点击进入Vue源码的调用函数或代码行。
  2. 上述生命周期钩子后的加粗字体是Vue文档对钩子函数的简要解释。
  3. 文中引入的Vue源码均进行了不同程度的简化,仅供参考,详细代码可以通过第一条说明位置点击查看。

beforeCreate & created


Vue.prototype._init = function (options?: Object) {
  // ...
  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')
  // ...
}

上面的代码是Vue实例化时调用的方法,从代码中我们可以看到,Vue的实例化阶段执行了 beforeCreate 和 created 两个钩子函数,下面分别来说。

beforeCreate

  • beforeCreate官方的解释是,在实例(Vue)初始化之后,数据观测(data observer)和 event/watcher 事件配置之前被调用
从上面的代码里可以看到,在调用 beforeCreate 之前,调用了三个函数,分别是初始化生命周期、事件和render。需要注意的是,此处的 initEvents(点击查看源码) 初始化的并不是自定义的事件,而是Vue一些原生事件和方法。
所以此时定义在 data 中的属性、methods中的方法等等都还不能访问。

created

  • created:官方解释是在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
在 beforeCreate之后,created之前,执行了 initInjections(vm)、initState(vm)、initProvide(vm) 三个方法
但是此处留一个疑问,我也没有搞明白,为什么是先初始化 inject,后初始化 provide?欢迎大佬们指导(抱拳.jpg)

从上面调用的方法可以看到,在 created 阶段,我们已经可以访问到自定义的一些 数据、属性和方法等内容,但是依然没有DOM。此阶段我们可以获取一些页面初始化时就需要显示的数据,但是不能操作DOM。

beforeMount & mounted


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

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // 需要对组件渲染进行性能追踪时执行逻辑
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

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

beforeMount

  • beforeMount:在挂载之前被调用:相关的render函数首次被调用(该钩子在服务端渲染期间不被调用)
在 created 之后,beforeMount 之前,会检查 el 属性,el 属性决定了我们最后要把DOM挂载到哪儿,如果不存在 el,则检查是否手动调用了 vm.$mount(el) 。两个条件满足其一,则进行下一步,否则停止执行。下一步会检查 template 属性是否存在,如果不存在则检查外层是否存在满足 el 传入选择器条件的 HTML 元素,两个条件满足其一,则进入 mount 过程,否则报错。

满足以上条件后,调用 beforeMount 钩子

beforeMount 之后,通过 vm._render() 将代码渲染为 VNode,然后通过 vm._update() 将 VNode patch 到真实的 DOM。完成后执行 mounted 钩子。

mounted

  • mounted:实例被挂载以后调用,el 被替换为 vm.$el
mounted 不会保证所有的子组件都挂载完成。如果希望等到整个视图都渲染完毕,可以使用 $nextTick

mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}
这里有一点容易懵逼的是,在Vue文档中写的是在 beforeMount 之后用新创建的 vm.$el 替换 el,但是上面的源码中却看到在 beforeMount 之前执行了 vm.$el = el 。关于这个问题,在测试beforeMount 和 mounted 两个钩子中输出 $el 后,我的个人理解是:
  1. 在 beforeMount 之前对 $el 的赋值只是把通过 el 选择器拿到的DOM给了 $el,但此时并没有我们写的其他页面内容,所以拿到相当于只是一个空壳。
  2. 在 beforeMount 之后,将代码渲染为 VNode,并通过 vm._update() patch 到真实DOM,通过代码可以看到,在 vm._update 中是更新过 vm.$el 的,所以此时的 $el 拿到的才是完成的 DOM 结构。因此文档说的是在 beforeMount 之后将 el 替换为新创建的 $el。

所以,这两个生命周期的执行逻辑可以总结为:
  1. vm.$el = el
  2. 执行 beforeMount()
  3. 调用 vm._render() 渲染 VNode
  4. vm._update() 把 VNode patch 到真实的 DOM,并更新 $el
  5. 执行 mounted()

beforeUpdate & updated


// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  
  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  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')
    }
  }
}

beforeUpdate

  • beforeUpdate:数据更新时调用,发生在虚拟DOM打补丁之前。这里适合在更新之前访问现有的DOM
上面代码对 vue 实例创建了一个监听,并将 updateComponent 作为回调,在实例数据有更新时去更新DOM。

但在此之前,有一个判断条件,也就是 before 参数中的内容,首先判断当前 _isMounted 为 true,也就是保证现在组件已经 mounted,同时 _isDestoryed 为 false,也就是保证现在组件数据不是因为要销毁才发生的改变。满足这两个条件后调用 beforeMount 钩子。

updated

  • updated:数据更改导致的虚拟DOM重新渲染和打补丁,之后调用该钩子。此时组件的DOM已经更新,所以可以执行依赖于DOM的操作
updated 调用在 callUpdatedHooks() 方法中,callUpdatedHooks() 在 flushSchedulerQueue() 中被调用。

flushSchedulerQueue() 主要是对要更新的队列进行预处理,从上面代码保留的注释中我们可以看到简要的处理逻辑。

在预处理完成后,将处理过的队列作为参数调用 callUpdatedHooks() ,方法内部对更新队列进行遍历,然后对满足条件的队列调用 updated 钩子。

beforeDestroy & destroy


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


beforeDestroy

  • beforeDestroy:实例销毁之前调用。在这一步,实例完全可用
beforeDestroy 在 $destroy 方法最开始调用,此时销毁还没有开始,所以当前实例完全可用。

beforeDestroy 之后,将 _isBeingDestroyed 置为true,同时开始执行一系列的销毁过程,主要包括:从当前组件的 $parent 中删除自己、移除watch、调用当前渲染 VNode 的销毁钩子。

上述过程执行完以后,调用 destroyed 钩子。

destoryed

  • destroyed:实例销毁后调用。该钩子被调用后,对应的Vue实例所有指令都被解绑,所有的事件监听器被移除,所有的子实例被销毁

至此,生命周期介绍完了,通过了解生命周期,我们可以简单的总结出以下几点:
  1. 在created中可以访问到自定义的数据、方法、计算属性、监听等内容,但是没有DOM,此时我们可以进行页面渲染所需数据的获取工作。
  2. 执行mounted时,DOM已经渲染完成,可以进行DOM的更新动作。
  3. 在destroyed中可以进行DOM的销毁工作。

了解了每个阶段可以做什么,能够很大程度上减少我们写代码中遇到的问题。如发现文章中的问题,欢迎交流、指出~


参考内容:

  1. 《Vue.js 技术揭秘》
  2. 《Vue官方文档》

感谢大佬们文章的帮助~