每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。
先上一张大图:
callHook
源码中最终执行生命周期的函数都是调用 callHook 方法,它的定义在 src/core/instance/lifecycle 中:
1 | export function callHook(vm: Component, hook: string) { |
- 选项合并时会把生命周期钩子选项合并成一个数组
- 遍历对应 hook 的数组,执行 invokeWithErrorHandling
- 判断是否 vm._hasHookEvent 触发相应的事件侦听器
invokeWithErrorHandling
这个函数定义在:src/core/util/error.js
1 | function invokeWithErrorHandling(handler, context, args, vm, info) { |
- 调用 handler,同时绑定 this,这样我们在钩子回调就能通过 this 访问到 vm 实例了
- 钩子如果返回一个 promise,那么给这个 promise 绑定一个 catch 函数
- 同时 _handled 保证了只绑定一次 catch
_hasHookEvent
vm._hasHookEvent 是在 initEvents 函数中定义的,它的作用是判断是否存在生命周期钩子的事件侦听器,初始化值为 false 代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将 vm._hasHookEvent 设置为 true 介绍下生命周期钩子事件帧听器:
1 | <child |
到这里就把 callHook 的逻辑给介绍完了,下面来看看 callHook 都在什么时候调用
beforeCreate & created
这两个钩子是在 _init 方法中执行的
1 | Vue.prototype._init = function (options) { |
- beforeCreate 和 created 的钩子调用是在 initState 的前后执行的
- initState 的作用是初始化 props、data、methods、watch、computed 等属性。
- 所以 beforeCreate 的钩子函数中就不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。
- 在这俩个钩子函数执行的时候,并没有渲染 DOM,所以我们也不能够访问 DOM
beforeMount & mounted
beforeMount 钩子函数发生在 mount,也就是 DOM 挂载之前,它的调用时机是在 mountComponent
函数中,定义在 src/core/instance/lifecycle.js
中:
1 | function mountComponent(vm, el, hydrating) { |
- 在执行
vm._render()
函数渲染 VNode 之前,执行了beforeMount
钩子函数 - 在执行完
vm._update()
把 VNode patch 到真实 DOM 后,执行mounted
钩子。
注意,这里对 mounted 钩子函数执行有一个判断逻辑,vm.$vnode
如果为 null,则表明这不是一次组件的初始化过程,而是我们通过外部 new Vue
初始化过程。那么对于组件,它的 mounted 时机在哪儿呢?
组件的 mounted
之前我们提到过,组件的 VNode patch 到 DOM 后,会执行 invokeInsertHook
函数,把 insertedVnodeQueue 里保存的钩子函数依次执行一遍,它的定义在 src/core/vdom/patch.js
中:
1 | return function patch(oldVnode, vnode, hydrating, removeOnly) { |
isInitialPatch
表示这是组件的 patch 上下文还是根实例的 patch 上下文- 调用 invokeInsertHook
1 | function invokeInsertHook(vnode, queue, initial) { |
该函数会执行 insert 这个钩子函数,对于组件而言,insert 钩子函数的定义在 src/core/vdom/create-component.js
中的 componentVNodeHooks 中:
1 | const componentVNodeHooks = { |
可以看到,每个子组件都是在这个钩子函数中执行 mounted 钩子函数,并且我们之前分析过,insertedVnodeQueue
的添加顺序是先子后父,所以对于同步渲染的子组件而言,mounted 钩子函数的执行顺序也是先子后父。
结合例子分析组件的 mounted
假设现在我们有以下例子:
1 | const App = { |
红色的标号表示步骤:
步骤 4:invokeInsertHook
- 此时的 vnode 是 App 组件的渲染 vnode
- 因为 App 组件已经是最深的那个组件了,所以此时的 queue 是一个空数组
- 通过 vnode.parent 拿到 App 组件的占位符 vnode
- 将 queue 临时保存到占位符 vnode 上
步骤 5: initComponent
1 | function initComponent(vnode, insertedVnodeQueue) { |
- 此时的 vnode 就是 App 组件的占位符 vnode
- 将占位符 vnode 上的临时数组 push 到队列中
- isPatchable 返回 true,执行 invokeCreateHooks 方法
步骤 6:invokeCreateHooks
1 | function invokeCreateHooks(vnode, insertedVnodeQueue) { |
- 判断 vnode 是否定义了 hook,如果是的话则表明这是一个占位符 vnode
- 将占位符 vnode 推进队列中 此时的
insertedVnodeQueue
状态:
步骤 7:invokeInsertHook
- 由于此时的上下文已经是根实例了,所以走的是 else 逻辑
- 遍历 insertedVnodeQueue 队列,执行 insert 钩子,在这个钩子又会执行 mounted 钩子
总结:
- beforeMount 先父后子
- mounted 先子后父
beforeUpdate & updated
顾名思义,beforeUpdate 和 updated 的钩子函数执行时机都应该是在数据更新的时候。beforeUpdate
的执行时机是在渲染 Watcher 的 before 函数中,我们刚才提到过:
1 | export function mountComponent( |
注意这里有个判断,也就是在组件已经 mounted 之后,才会去调用这个钩子函数。
update
的执行时机是在 flushSchedulerQueue
函数调用的时候,它的定义在 src/core/observer/scheduler.js
中:
1 | function flushSchedulerQueue() { |
- 遍历 queue,执行了 before 函数,从而执行了 beforeUpdate 函数
- 调用 callUpdatedHooks 函数,参数 updatedQueue 是更新了的 wathcer 数组
在组件 mount 的过程中,会实例化一个渲染的 Watcher 去监听 vm 上的数据变化重新渲染,这段逻辑发生在 mountComponent
函数执行的时候,那么在实例化 Watcher 的过程中,在它的构造函数里会判断 isRenderWatcher
,接着把当前 watcher 的实例赋值给 vm._watcher
,定义在 src/core/observer/watcher.js
中:
1 | export default class Watcher { |
同时,还把当前 wathcer 实例 push 到 vm._watchers
中,vm._watcher
是专门用来监听 vm 上数据变化然后重新渲染的,所以它是一个渲染相关的 watcher,updatedQueue
是更新了的 wathcer 数组,因此在 callUpdatedHooks
函数中,它对这些数组做遍历,只有满足当前 watcher 为 vm._watcher
以及组件已经 mounted
这两个条件,才会执行 updated 钩子函数。
总结:
- beforeUpdate 先父后子
- updated 先子后父
beforeDestroy & destroyed
顾名思义,beforeDestroy 和 destroyed 钩子函数的执行时机在组件销毁的阶段,组件的销毁过程之后会详细介绍,最终会调用 $destroy 方法
,它的定义在 src/core/instance/lifecycle.js
中:
1 | Vue.prototype.$destroy = function () { |
- beforeDestroy 钩子函数的执行时机是在
$destroy
函数执行最开始的地方 - 接着执行了一系列的销毁动作
- 包括从 parent 的
$children
中删掉自身 - 删除 watcher
- 当前的 VNode 执行销毁钩子函数等
- 包括从 parent 的
- 执行 vm.__patch__(vm._vnode, null) 触发它子组件的销毁钩子函数,这样一层层的递归调用
- 执行完毕后再调用 destroy 钩子函数。
总结一下:
beforeDestroy 先父后子
destroyed 先子后父
activated & deactivated
activated 和 deactivated 钩子函数是专门为 keep-alive 组件定制的钩子,在介绍 keep-alive
组件的时候详细介绍
总结
这一节主要介绍了 Vue 生命周期中各个钩子函数的执行时机以及顺序,通过分析,我们知道了如:
在 created 钩子函数中可以访问到数据
在 mounted 钩子函数中可以访问到 DOM
在 destroy 钩子函数中可以做一些定时器销毁工作