Vue 生命周期钩子函数实现原理
原文分析的是 Vue 2 的
callHook和invokeWithErrorHandling。这些内容仍适合学习 Vue 2 的源码,但不能直接当作 Vue 3 的实现。本文保留 Vue 2 源码主线,并补充 Vue 3.5 的生命周期注册与错误处理方式。
一、生命周期钩子到底做了什么?
无论是 Vue 2 还是 Vue 3,生命周期钩子都可以抽象为:
- 框架在组件实例上保存某类生命周期回调;
- 到达对应阶段时取出回调列表;
- 按顺序执行回调;
- 通过统一的错误处理函数处理同步异常和异步 Promise 异常;
- 在组件卸载、KeepAlive 激活/停用、SSR 等场景使用不同的调度时机。
Vue 2 会把同名的 mixin、extends 和组件自身钩子合并为数组;Vue 3 则把 Composition API 注册的钩子放到组件内部对应的 hook 数组中。
二、Vue 2 的 beforeCreate:从 callHook 开始
Vue 2.7 的源码位置是:
在 Vue 2 中,调用生命周期钩子的入口可以简化为:
export function callHook(
vm: Component,
hook: string,
...args: any[]
) {
// 暂时关闭依赖收集,避免钩子执行过程被错误地记录为当前 watcher 的依赖
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
invokeWithErrorHandling(
handlers[i],
vm,
args.length ? args : null,
vm,
info,
)
}
}
// 支持 vm.$on('hook:mounted', handler) 这类生命周期事件监听
if (vm._hasHookEvent) {
vm.$emit(`hook:${hook}`)
}
popTarget()
}
上面的代码是解释版,重点不在参数的逐字复制,而在执行顺序:
- 从
vm.$options找到当前钩子; - 钩子通常已经在配置合并阶段变成数组;
- 按数组顺序调用每个处理函数;
- 执行前后通过
pushTarget()/popTarget()暂停和恢复依赖收集; - 最后触发
hook:xxx事件。
这里的 pushTarget() 不是为了“防止重复定义响应式数据”,而是为了避免执行生命周期函数时,当前正在运行的 watcher 错误收集到钩子内部读取的响应式依赖。它操作的是 Vue 2 响应式系统中的 Dep.target 栈。
beforeCreate 的调用位置
Vue 2 初始化实例时,大致顺序如下:
初始化 lifecycle / events / render
-> beforeCreate
-> 初始化 injections / props / methods / data / computed / watch
-> created
-> 判断是否有 el/template
-> beforeMount
-> mounted
因此在 Vue 2 的 beforeCreate 中,data、props、computed 和 watch 还没有全部完成初始化。created 时响应式 data 和 methods 已经准备好,但 DOM 还没有挂载。
三、Vue 2 的异常处理:invokeWithErrorHandling
原文分析的函数可以整理为:
export function invokeWithErrorHandling(
handler: Function,
context: any,
args: any[] | null,
vm: any,
info: string,
) {
let res
try {
res = args
? handler.apply(context, args)
: handler.call(context)
if (
res &&
!res._isVue &&
isPromise(res) &&
!res._handled
) {
res.catch(error => {
handleError(error, vm, `${info} (Promise/async)`)
})
// Vue 2 内部标记,避免嵌套错误处理重复注册 catch
res._handled = true
}
} catch (error) {
handleError(error, vm, info)
}
return res
}
这里需要注意三点:
- 同步异常由
try/catch交给handleError; - 如果钩子返回 Promise,Vue 2 会额外监听 rejected Promise;
- 错误会继续经过组件的
errorCaptured和应用级错误处理器。
这也是为什么下面这种异步生命周期代码不会因为没有手写 try/catch 就完全脱离 Vue 的错误处理:
export default {
async created() {
const data = await fetch('/api/user').then(response => response.json())
this.user = data
},
}
但这不代表可以忽略业务错误。请求取消、接口状态码和用户提示仍然应该按业务需要处理。
四、Vue 3 的实现方式发生了变化
Vue 3 不再使用 Vue 2 的 vm.$options[hook]、pushTarget() 和 vm.$emit('hook:xxx') 作为 Composition API 生命周期的主实现。相关源码位置是:
runtime-core/src/apiLifecycle.tsv3.5.39runtime-core/src/errorHandling.tsv3.5.39runtime-core/src/renderer.tsv3.5.39
Composition API 的注册入口可以概括为:
export function injectHook(
type: LifecycleHooks,
hook: Function,
target = currentInstance,
prepend = false,
) {
if (!target) {
// 开发环境会提示:生命周期 hook 必须在 setup 中注册
return
}
const hooks = target[type] || (target[type] = [])
const wrappedHook = hook.__weh || (hook.__weh = (...args: unknown[]) => {
pauseTracking()
const reset = setCurrentInstance(target)
const result = callWithAsyncErrorHandling(
hook,
target,
type,
args,
)
reset()
resetTracking()
return result
})
if (prepend) hooks.unshift(wrappedHook)
else hooks.push(wrappedHook)
return wrappedHook
}
这同样是保留结构的简化片段。Vue 3 的关键点是:
currentInstance表示当前正在执行setup()的组件;onMounted、onUpdated、onUnmounted等 API 本质上调用injectHook;- hook 会包装为带错误处理和当前实例上下文的函数;
pauseTracking()防止执行 hook 时错误影响响应式依赖追踪;- Composition API 生命周期必须在
setup()同步执行阶段注册,不能在setTimeout或await之后才注册。
例如:
<script setup>
import { onMounted, onUnmounted } from 'vue'
function handleResize() {
console.log(window.innerWidth)
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
如果生命周期钩子在异步回调之后才注册,当前组件实例可能已经不再是 currentInstance,Vue 无法把它可靠地关联到组件上。
五、Vue 3 生命周期和 callWithAsyncErrorHandling
Vue 3 的公共生命周期 API 可以理解为两部分:
const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
const onMounted = createHook(LifecycleHooks.MOUNTED)
const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
const onUpdated = createHook(LifecycleHooks.UPDATED)
const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
组件渲染器在适当的阶段执行对应数组,并通过 callWithAsyncErrorHandling 处理回调:
onBeforeMount hooks
-> 首次 render / patch
-> queuePostRenderEffect(onMounted hooks)
onBeforeUpdate hooks
-> render / patch
-> queuePostRenderEffect(onUpdated hooks)
onBeforeUnmount hooks
-> 卸载子树
-> unmounted hooks
mounted、updated、unmounted 这类 hook 往往会作为 post-render effect 调度,具体执行时机还会受到组件更新队列、Suspense 和 Scheduler 的影响。不能把它们简单理解成普通函数调用后立刻同步执行。
Vue 3 没有 Vue 2 的 hook:mounted 主 API
Vue 2 可以使用:
vm.$on('hook:mounted', callback)
Vue 3 移除了组件实例上的 $on、$off、$once。跨组件通信应使用 props/emits、provide/inject 或明确的事件库;监听生命周期则使用 onMounted 等 Composition API,或者在 Options API 中直接声明 mounted()。
六、SSR 的差异
服务器端没有真实 DOM,因此:
onMounted、onUpdated、onUnmounted不会在 SSR 期间执行;- 需要在服务端预取数据时,可使用
onServerPrefetch; - DOM、
window、document操作应放到onMounted或客户端守卫中; - 组件 setup 中注册的普通生命周期仍然要遵循同步注册规则。
<script setup>
import { onMounted, onServerPrefetch, ref } from 'vue'
const user = ref(null)
async function loadUser() {
user.value = await fetch('/api/user').then(response => response.json())
}
onServerPrefetch(loadUser)
onMounted(() => {
// 只执行浏览器相关逻辑
console.log(document.title)
})
</script>
七、一句话总结
- Vue 2:生命周期回调保存在
vm.$options,通过callHook遍历执行,invokeWithErrorHandling负责异常和 Promise 错误。 - Vue 2 的
pushTarget()/popTarget()是响应式依赖收集边界,不是响应式数据定义逻辑。 - Vue 3:
onMounted等 API 在 setup 同步阶段通过injectHook注册,执行时使用当前实例和统一错误处理。 - Vue 3 的
onMounted、onUpdated等钩子受到 Scheduler、post-render effect、Suspense 和 SSR 的影响。 - Vue 2 的
$on('hook:xxx')、vm.$options和callHook代码不能直接复制到 Vue 3。
官方参考
- Vue 3 生命周期指南
- Vue 3 Composition API 生命周期
- Vue 3 Options API 生命周期
- Vue 3
apiLifecycle.tsv3.5.39 - Vue 3 错误处理源码 v3.5.39
- Vue 2.7
lifecycle.ts - Vue 2.7
error.ts - Vue 3 SSR 数据预取
原文出处
作者:Fishing
原文链接:https://juejin.cn/post/7017001556243382308
来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。