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

生命周期的是咱们在开发中不可回避的话题。了解生命周期也可让咱们知道什么阶段能够作什么,以及更好的解决项目中遇到的问题。
本文包含我的理解内容,但愿你们批判性阅读,若有问题欢迎交流~


文章说明

  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的销毁工做。

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


参考内容:javascript

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

感谢大佬们文章的帮助~html

相关文章
相关标签/搜索