技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 中父组件能否监听子组件的生命周期?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/40-Vue中父组件能否监听子组件的生命周期?
本文目录7 个章节
  1. 一、使用显式事件通知:最稳定、最推荐
  2. 二、Vue 2:使用 hook: 事件
  3. 三、Vue 3:hook: 实例事件已经不是原来的 API
  4. 四、生命周期监听、显式事件和 KeepAlive 的选择
  5. 五、总结
  6. 官方参考
  7. 原文出处

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>。原文引用的源码截图表达的就是这个过程。

Vue 2 hook 事件源码截图(本地化,历史资料)

Vue 2 hook 事件源码截图(本地化,历史资料)

@hook: 的限制

  1. 它是 Vue 2 实例事件机制的一部分,不是浏览器标准事件。
  2. 它只观察当前子实例的对应钩子,不代表所有后代组件都已经完成。
  3. mounted 不代表异步组件的 loader、接口请求或图片加载完成。
  4. KeepAlive 场景中,组件切换进出缓存应关注 activated/deactivated,不能只监听 mounted/destroyed
  5. 如果只是为了执行业务初始化,直接监听生命周期会让父组件依赖子组件实现细节,优先使用 readyloaded 等显式事件。

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 2Vue 3.5
子组件业务准备完成$emit('ready')defineEmits + emit('ready')
观察当前 VNode 已挂载@hook:mounted@vue:mounted
观察组件实例创建/销毁@hook:created@hook:destroyed在子组件自身使用生命周期或显式事件;VNode 监听不覆盖 created
组件进入/离开 KeepAlive 缓存activated/deactivatedonActivated/onDeactivated
调用子组件公开方法$refstemplate 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.7.16、Vue 3.5.39 官方文档与源码补充版本说明。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS