Vue 中父组件能否监听子组件的生命周期?
结论:可以,但要先区分 Vue 2 的
hook:*实例事件、Vue 3 的 VNode 生命周期监听,以及更推荐的业务事件。本文保留原文“父组件监听子组件生命周期”的问题和两种思路,并补充 Vue 2.7.16、Vue 3.5.39 的版本边界。
一、使用显式事件通知:最稳定、最推荐
如果父组件真正关心的是“子组件已经准备好”“数据已经加载完成”,最清晰的方式是由子组件主动发出一个有业务含义的事件。
Vue 2.7
<!-- Parent.vue -->
<Child @ready="handleChildReady" />
// Child.vue
export default {
mounted() {
this.$emit('ready')
},
methods: {
// ...
},
}
Vue 3.5
<!-- Child.vue -->
<script setup lang="ts">
import { onMounted } from 'vue'
const emit = defineEmits<{
ready: []
}>()
onMounted(() => {
emit('ready')
})
</script>
<!-- Parent.vue -->
<script setup lang="ts">
function handleChildReady() {
console.log('子组件已准备好')
}
</script>
<template>
<Child @ready="handleChildReady" />
</template>
这种方式的优点是父子组件之间约定的是业务语义,而不是 Vue 内部的生命周期名称。mounted 只表示组件挂载完成,并不表示接口请求、图片加载或其他初始化工作已经完成;如果父组件需要等待这些工作,就应该让子组件在真正完成时触发 ready,必要时还可以携带数据或错误信息:
this.$emit('ready', { data: this.data })
// Vue 3:emit('ready', { data: data.value })
显式事件也更容易迁移到 Vue 3、测试和异步组件场景。
二、Vue 2:使用 hook: 事件
原文第二种方法使用的是 Vue 2 的 @hook: 事件:
<Child
@hook:created="onChildCreated"
@hook:mounted="onChildMounted"
@hook:updated="onChildUpdated"
@hook:beforeDestroy="onChildBeforeDestroy"
@hook:destroyed="onChildDestroyed"
/>
它不是浏览器 DOM 事件,也不是子组件手动 $emit('hook:mounted') 产生的普通自定义事件。Vue 2 会在调用组件生命周期钩子后,针对相应的实例发出 hook:<生命周期名> 事件。因此父组件可以监听当前子组件的生命周期。
最常用的写法是:
<Child @hook:mounted="handleChildMounted" />
export default {
methods: {
handleChildMounted() {
console.log('Child 的 mounted 已完成')
},
},
}
Vue 2 源码中的工作过程
Vue 2.7.16 的相关实现已经是 TypeScript,主要可以在以下文件中看到:
简化流程如下:
父组件在 Child VNode 上注册 @hook:mounted
↓
Vue 2 将监听器安装到 Child 实例
↓
Child 执行 mounted 钩子
↓
Vue 2 调用 Child.$emit('hook:mounted')
↓
父组件的 handleChildMounted 被调用
events.ts 会识别以 hook: 开头的监听器,并设置实例上的 _hasHookEvent 标志;lifecycle.ts 中的 callHook() 执行对应生命周期钩子后,会发出 hook:<hookName>。原文引用的源码截图表达的就是这个过程。


@hook: 的限制
- 它是 Vue 2 实例事件机制的一部分,不是浏览器标准事件。
- 它只观察当前子实例的对应钩子,不代表所有后代组件都已经完成。
mounted不代表异步组件的 loader、接口请求或图片加载完成。- KeepAlive 场景中,组件切换进出缓存应关注
activated/deactivated,不能只监听mounted/destroyed。 - 如果只是为了执行业务初始化,直接监听生命周期会让父组件依赖子组件实现细节,优先使用
ready、loaded等显式事件。
Vue 2 中的自清理示例
Vue 2 官方文档也展示过使用 hook:beforeDestroy 做资源清理的方式:
export default {
mounted() {
const picker = new Pikaday({
field: this.$refs.input,
})
this.$once('hook:beforeDestroy', () => {
picker.destroy()
})
},
}
这里监听的是当前组件自己的生命周期,并不是父组件监听子组件;但它说明了 hook:* 的本质是 Vue 2 实例事件。官方参考:Programmatic Event Listeners。
三、Vue 3:hook: 实例事件已经不是原来的 API
Vue 3 标准构建移除了组件实例的 $on、$off、$once,因此下面这种 Vue 2 写法不能直接迁移:
// Vue 3 中不要这样写
child.$on('hook:mounted', handler)
child.$once('hook:mounted', handler)
Vue 3 仍然使用 $emit/emit 处理父子自定义事件,但不能再依赖 Vue 2 的实例事件总线。参考:Vue 3 Events API 迁移指南。
Vue 3 的 VNode 生命周期监听
如果确实需要观察一个子组件 VNode 的挂载、更新或卸载阶段,Vue 3.5.39 模板中使用 @vue: 前缀:
<Child
@vue:mounted="onChildMounted"
@vue:updated="onChildUpdated"
@vue:beforeUnmount="onChildBeforeUnmount"
@vue:unmounted="onChildUnmounted"
/>
这些是 VNode 生命周期钩子,不是 Vue 2 那种可以监听任意组件实例钩子的通用事件。Vue 3.5.39 支持的主要 VNode 钩子包括:
@vue:beforeMount@vue:mounted@vue:beforeUpdate@vue:updated@vue:beforeUnmount@vue:unmounted
不要写成 @vue:created 或 @vue:activated,它们不是对应的通用 VNode 钩子。KeepAlive 的激活/停用应由子组件使用 onActivated() / onDeactivated() 自己处理。
在渲染函数中,VNode 钩子的 prop 名称是 onVnodeMounted 等:
import { h } from 'vue'
h(Child, {
onVnodeMounted(vnode) {
console.log('Child VNode 已挂载', vnode)
},
})
版本边界也要注意:Vue 3.2.25 引入 vue: 别名,3.3 起旧的 @vnode-* 写法被弃用,3.4 起旧前缀在模板中可能直接被编译器拒绝。Vue 3.5 项目应使用 @vue:*,不要照搬网上的 @vnode-mounted 示例。参考:VNode 生命周期事件迁移指南。
Vue 3 的组件 ref
如果父组件需要调用子组件公开的方法,而不是监听生命周期,可以使用 template ref。<script setup> 子组件默认不会把内部绑定暴露给父组件,需要显式 defineExpose:
<!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const input = ref<HTMLInputElement | null>(null)
function focus() {
input.value?.focus()
}
defineExpose({ focus })
</script>
<template>
<input ref="input" />
</template>
<!-- Parent.vue -->
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
const child = ref<{ focus: () => void } | null>(null)
onMounted(async () => {
await nextTick()
child.value?.focus()
})
</script>
<template>
<Child ref="child" />
</template>
ref 适合调用公开的命令式方法,不适合用来替代 props 和 emits 传递普通状态。
四、生命周期监听、显式事件和 KeepAlive 的选择
| 需求 | Vue 2 | Vue 3.5 |
|---|---|---|
| 子组件业务准备完成 | $emit('ready') | defineEmits + emit('ready') |
| 观察当前 VNode 已挂载 | @hook:mounted | @vue:mounted |
| 观察组件实例创建/销毁 | @hook:created、@hook:destroyed | 在子组件自身使用生命周期或显式事件;VNode 监听不覆盖 created |
| 组件进入/离开 KeepAlive 缓存 | activated/deactivated | onActivated/onDeactivated |
| 调用子组件公开方法 | $refs | template ref + defineExpose |
如果业务代码出现大量 @hook:* 或 @vue:*,通常说明父组件正在依赖子组件的内部实现。可以考虑重新划分 props、emits、composable 或状态管理边界。
五、总结
- Vue 2 可以使用
@hook:mounted、@hook:updated等监听子组件实例生命周期。 - Vue 2 的
@hook:最终仍然通过实例$emit工作,原文“框架已经帮我们处理好了”的理解是正确的。 - Vue 3 移除了
$on/$off/$once,不能继续使用child.$on('hook:mounted')。 - Vue 3.5 如果确实要监听 VNode 阶段,使用
@vue:mounted等 VNode 生命周期监听;render 函数对应onVnodeMounted。 - 对业务初始化、数据加载和组件就绪状态,显式
ready/loaded事件比监听生命周期更清晰。 - KeepAlive 的缓存切换使用 activated/deactivated 语义,不应把它当成 mounted/unmounted。
官方参考
- Vue 2
$on/$onceAPI - Vue 2 Programmatic Event Listeners
- Vue 2.7.16 events.ts
- Vue 2.7.16 lifecycle.ts
- Vue 3 Events API 迁移指南
- Vue 3 VNode Lifecycle Events 迁移指南
- Vue 3 Composition API 生命周期
- Vue 3 KeepAlive
原文出处
作者:大钟同学
来源:稀土掘金
本文保留原文的两种思路,并根据 Vue 2.7.16、Vue 3.5.39 官方文档与源码补充版本说明。