beforeUpdate
在初始化阶段,vue实例中的数据(data/props/computed/watch)已经被处理成响应式的了。任何对数据的访问(getter)都会被watcher
添加为依赖,任何对数据的变更,都将触发对数据有依赖的watcher
的更新。watcher
在更新之前会调用watcher.before
方法,该方法是在挂载阶段创建watcher
实例的时候定义的。
watcher.before
中会触发beforeUpdate
钩子。此时vue实例只是确定了最终需要更新的数据,尚未真正开始更新。
updated
从代码中可以看到,在触发updated
钩子前,vue实例需要对DOM元素进行更新。更新的过程是异步的。具体方式通过实例的render watcher
执行run
方法。该方法会去调用我们在挂载阶段介绍的updateComponent
函数。从而重新创建vnode,并进行vnode的diff操作后更新DOM元素。
我们还注意到,代码中调用了callActivatedHooks
函数,该函数用来触发activated
钩子。下文我们再做说明,这里不展开。
销毁
当vue实例的$destroy
方法时,实例将进入销毁阶段。此时触发的钩子是:
beforeDestory
和destroyed
。
// 销毁Vue实例 Vue.prototype.$destroy = function () { const vm: Component = this // 避免重复执行销毁操作 if (vm._isBeingDestroyed) { return } // 触发实例的beforeDestroy钩子 callHook(vm, 'beforeDestroy') vm._isBeingDestroyed = true // 将实例从其父实例中的$chilren中移除(断开与父实例的联系) const parent = vm.$parent if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm) } // 销毁实例的 watchers if (vm._watcher) { vm._watcher.teardown() } let i = vm._watchers.length while (i--) { vm._watchers[i].teardown() } if (vm._data.__ob__) { vm._data.__ob__.vmCount-- } vm._isDestroyed = true // 销毁指令、ref等 vm.__patch__(vm._vnode, null) // 触发destroyed事件 callHook(vm, 'destroyed') // 移除实例的所有事件监听器 vm.$off() if (vm.$el) { vm.$el.__vue__ = null } // 释放循环引用(#6759) if (vm.$vnode) { vm.$vnode.parent = null } } }
beforeDestroy
从代码中可以看到,$destroy
被调用时,在执行实际的销毁动作前触发beforeDestroy
。此时,由于并未开始执行实际的销毁代码,实例及DOM元素仍可正常访问。
destroyed
从代码中可以看到,销毁操作主要包括以下几点:清空所有的watcher
、删除所有的指令、删除DOM元素并关闭DOM上的所有的事件、断开与父实例之间的关系、将实例标记成已销毁状态。此时实例已经被销毁,已经无法访问实例的属性和DOM元素了。
keep-alive包裹下的组件的生命周期钩子
下面的两个钩子只有当组件包裹在keep-alive
时才会触发。
activated
HTML标签和组件标签在vue内部实现中都有对应的vnode,组件vnode在设计上与普通的HTML标签的vnode有所不同。例如组件vnode上包含init
、prepatch
、insert
、destroy
等钩子。这些钩子在组件实例初始化、更新和销毁等不同的阶段进行调用。
// 组件vnode的钩子 const componentVNodeHooks = { // ...省略其他钩子 insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true // 触发组件的mounted钩子 callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } } } // 触发activated钩子 export function activateChildComponent (vm: Component, direct?: boolean) { // ...省略部分代码 if (vm._inactive || vm._inactive === null) { vm._inactive = false for (let i = 0; i < vm.$children.length; i++) { activateChildComponent(vm.$children[i]) } // 调用实例的activated钩子 callHook(vm, 'activated') } }
与根实例一样,keep-alive
包裹下的组件实例初始化时同样会依次经历初始化阶段、挂载阶段,但在挂载阶段之后会调用组件vnode的insert
钩子,insert
钩子会触发组件实例的activated
钩子。因为insert
是在组件实例挂载完成后调用的,所以mounted
的触发早于activated
。
当组件切换回来的同时,组件的数据发生了变化,此时组件将进入更新阶段,意味着将会依次触发beforeUpdate
和updated
钩子。
那么问题来了,activated
、beforeUpdate
和updated
钩子哪个先触发呢?
答案是先触发beforeUpdate
,再触发activated
,最后触发updated
。
我们回顾下前面更新阶段的代码:
// 触发activated钩子 callActivatedHooks(activatedQueue) // 触发updated钩子 callUpdatedHooks(updatedQueue) //...省略部分代码 // 触发activated钩子 function callActivatedHooks (queue) { for (let i = 0; i < queue.length; i++) { queue[i]._inactive = true activateChildComponent(queue[i], true /* true */) } }
可以看到在函数flushSchedulerQueue
中,callActivatedHooks
函数用来触发activated
。而callActivatedHooks
的顺序在callUpdatedHooks
前面,所以activated
钩子的触发早于updated
钩子。
deactivated
const componentVNodeHooks = { // ... 省略其他钩子 destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { // 未被keep-alive包裹销毁组件 if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { // 被keep-alive包裹 deactivateChildComponent(componentInstance, true /* direct */) } } } } export function deactivateChildComponent (vm: Component, direct?: boolean) { if (direct) { vm._directInactive = true if (isInInactiveTree(vm)) { return } } if (!vm._inactive) { vm._inactive = true for (let i = 0; i < vm.$children.length; i++) { deactivateChildComponent(vm.$children[i]) } // 调用实例的deactivated钩子 callHook(vm, 'deactivated') } }
当组件被切换到其他的组件时,会调用组件vnode的destroy
钩子,在组件被keep-alive
包裹的情况下,只会将组件对应的DOM元素从DOM树删除,但不会销毁组件实例,此时会调用deactivateChildComponent
从而触发deactivated
钩子。组件在没有被keep-alvie
包裹的情况下,才会调用$destroy
销毁组件实例,触发beforeDestroy
和destroyed
钩子。
总结
关于vue实例的生命周期,官网讲解的其实是比较简单易懂的。本文主要还是希望能从源码的角度,进一步让大家理解每个生命周期做了什么处理。更好地理解钩子的触发时机及先后顺序。