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

生命周期的是我们在开发中不可回避的话题。了解生命周期也可以让我们知道什么阶段可以做什么,以及更好的解决项目中遇到的问题。

本文包含个人理解内容,希望大家批判性阅读,如有问题欢迎交流~

1e0b32cbc14119e1d8f14f9731d424b5.png

文章说明

  1. 每一个 · 后跟的生命周期钩子可以点击进入Vue源码的调用函数或代码行。

  2. 上述生命周期钩子后的加粗字体是Vue文档对钩子函数的简要解释。

  3. 文中引入的Vue源码均进行了不同程度的简化,仅供参考,详细代码可以通过第一条说明位置点击查看。

beforeCreate & created

70d1a90ea1d617678c050084ab0faddf.png

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官方的解释是,在实例(Vue)初始化之后,数据观测(data observer)和 event/watcher 事件配置之前被调用

从上面的代码里可以看到,在调用 beforeCreate 之前,调用了三个函数,分别是初始化生命周期、事件和render。需要注意的是,此处的 initEvents(点击查看源码) 初始化的并不是自定义的事件,而是Vue一些原生事件和方法。

所以此时定义在 data 中的属性、methods中的方法等等都还不能访问。

  • created:官方解释是在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el  property 目前尚不可用。

在 beforeCreate之后,created之前,执行了 initInjections(vm)、initState(vm)、initProvide(vm) 三个方法

  • initInjections(vm):初始化注入信息

  • initState(vm):初始化props、methods、data、computed、watch等内容

  • initProvide(vm):初始化provide信息

但是此处留一个疑问,我也没有搞明白,为什么是先初始化 inject,后初始化 provide?欢迎大佬们指导(抱拳.jpg)

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

beforeMount & mounted

b81e54eda10130266f1a116d56812311.png

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:在挂载之前被调用:相关的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:实例被挂载以后调用,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

4b0ddf58f0e0ac96677f18cb07472a04.png

// 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 definednew 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:数据更新时调用,发生在虚拟DOM打补丁之前。这里适合在更新之前访问现有的DOM

上面代码对 vue 实例创建了一个监听,并将 updateComponent 作为回调,在实例数据有更新时去更新DOM。

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

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

updated 调用在 callUpdatedHooks() 方法中,callUpdatedHooks() 在 flushSchedulerQueue() 中被调用。

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

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

beforeDestroy & destroy

5fab579a6709d666927b204fb2c5d547.png

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 在 $destroy 方法最开始调用,此时销毁还没有开始,所以当前实例完全可用。

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

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

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

至此,生命周期介绍完了,通过了解生命周期,我们可以简单的总结出以下几点:

  1. 在created中可以访问到自定义的数据、方法、计算属性、监听等内容,但是没有DOM,此时我们可以进行页面渲染所需数据的获取工作。

  2. 执行mounted时,DOM已经渲染完成,可以进行DOM的更新动作。

  3. 在destroyed中可以进行DOM的销毁工作。

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

8adbf4040b6d9f3193d079ae95a8233c.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值