生命周期的是我们在开发中不可回避的话题。了解生命周期也可以让我们知道什么阶段可以做什么,以及更好的解决项目中遇到的问题。
本文包含个人理解内容,希望大家批判性阅读,如有问题欢迎交流~
文章说明
每一个 · 后跟的生命周期钩子可以点击进入Vue源码的调用函数或代码行。
上述生命周期钩子后的加粗字体是Vue文档对钩子函数的简要解释。
文中引入的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:官方的解释是,在实例(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
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 后,我的个人理解是:
在 beforeMount 之前对 $el 的赋值只是把通过 el 选择器拿到的DOM给了 $el,但此时并没有我们写的其他页面内容,所以拿到相当于只是一个空壳。
在 beforeMount 之后,将代码渲染为 VNode,并通过 vm._update() patch 到真实DOM,通过代码可以看到,在 vm._update 中是更新过 vm.$el 的,所以此时的 $el 拿到的才是完成的 DOM 结构。因此文档说的是在 beforeMount 之后将 el 替换为新创建的 $el。
所以,这两个生命周期的执行逻辑可以总结为:
vm.$el = el
执行 beforeMount()
调用 vm._render() 渲染 VNode
vm._update() 把 VNode patch 到真实的 DOM,并更新 $el
执行 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 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
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实例所有指令都被解绑,所有的事件监听器被移除,所有的子实例被销毁
至此,生命周期介绍完了,通过了解生命周期,我们可以简单的总结出以下几点:
在created中可以访问到自定义的数据、方法、计算属性、监听等内容,但是没有DOM,此时我们可以进行页面渲染所需数据的获取工作。
执行mounted时,DOM已经渲染完成,可以进行DOM的更新动作。
在destroyed中可以进行DOM的销毁工作。
了解了每个阶段可以做什么,能够很大程度上减少我们写代码中遇到的问题。如发现文章中的问题,欢迎交流、指出~