前端知库

Vue 2 与 Vue 3 的生命周期如何对照?父子组件钩子按什么顺序执行?

Vue 2 的 beforeDestroy/destroyed 在 Vue 3 中改名为 beforeUnmount/unmounted,组合式 API 用 onXxx 注册。父子组件创建时父先开始、子先挂载完成;更新和销毁同样是父先开始、子先结束。

参考答案

Vue 2 已于 2023-12-31 停止维护,本题 Vue 2 部分以 2.7 为准。何时请求数据、何时清理资源见《Vue 生命周期中何时访问 DOM、发请求和清理资源?》,本题只讲版本对照与执行顺序。

简答

  • 选项式钩子在 Vue 3 中基本保留,只有两个改名:beforeDestroy → beforeUnmount,destroyed → unmounted。
  • 组合式 API 没有 beforeCreate/created 对应函数,setup() 本身承担这部分工作,并且在所有选项式钩子之前执行,包括 beforeCreate。
  • 创建阶段:父组件执行到 beforeMount 后开始渲染子组件,子组件全部 mounted 后父组件才 mounted。
  • 更新阶段:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated。
  • 销毁阶段(Vue 2):父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed;Vue 3 的卸载顺序相同,只是名字换成 unmount。

钩子对照表

Vue 2 选项Vue 3 选项Vue 3 组合式 API(2.7 也可用)
beforeCreatebeforeCreate无,写在 setup() 中
createdcreated无,写在 setup() 中
beforeMountbeforeMountonBeforeMount
mountedmountedonMounted
beforeUpdatebeforeUpdateonBeforeUpdate
updatedupdatedonUpdated
beforeDestroybeforeUnmountonBeforeUnmount
destroyedunmountedonUnmounted
activated / deactivated同名onActivated / onDeactivated
errorCaptured同名onErrorCaptured

改名记录在迁移指南的“Other Minor Changes”中。Vue 2.7 把组合式 API 移植回 Vue 2,所以 onMounted、onBeforeUnmount 等也能在 2.7 中使用,便于升级前先改写。

父子顺序为什么是“父先开始、子先结束”

父组件渲染时才会创建子组件。父组件进入 beforeMount 后执行渲染和 patch,遇到子组件就完整走一遍子组件的创建与挂载;子组件的 DOM 插入父组件的 DOM 树后,父组件才算挂载完成。

更新时,调度器按 watcher 创建顺序(父先于子)运行渲染 watcher,所以父组件先触发 beforeUpdate;updated 在整个队列执行完后统一调用,Vue 2 源码 callUpdatedHooks 倒序遍历队列,因此子组件的 updated 先于父组件。

销毁时,父组件先触发 beforeDestroy,再递归销毁子组件,最后自己触发 destroyed。

示例:实际运行结果

JavaScript
// vue@2.7.16,在浏览器中运行
const log = [];
const names = ['beforeCreate', 'created', 'beforeMount', 'mounted',
  'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed'];
const hooks = (who) => Object.fromEntries(
  names.map((h) => [h, () => log.push(`${who} ${h}`)]),
);

const Child = { props: ['msg'], template: '<i>{{ msg }}</i>', ...hooks('子') };
const vm = new Vue({
  data: { msg: 'a' },
  components: { Child },
  template: '<div><Child :msg="msg" /></div>',
  ...hooks('父'),
}).$mount();

vm.msg = 'b';            // 触发更新
Vue.nextTick(() => vm.$destroy());

输出按阶段整理如下:

text
创建:父 beforeCreate, 父 created, 父 beforeMount,
      子 beforeCreate, 子 created, 子 beforeMount, 子 mounted, 父 mounted
更新:父 beforeUpdate, 子 beforeUpdate, 子 updated, 父 updated
销毁:父 beforeDestroy, 子 beforeDestroy, 子 destroyed, 父 destroyed

把同一段代码换成 Vue 3.5(beforeUnmount/unmounted,app.unmount())运行,三个阶段的顺序完全一致。

常见追问

  • 子组件的 props 变化一定会触发父组件的 beforeUpdate 吗?不一定。上例中父组件模板读取了 msg,所以父组件也会更新;如果只有子组件依赖的数据变了,只有子组件更新。
  • 在父组件 mounted 中能访问子组件的 DOM 吗?能,子组件此时已挂载。但异步组件或 v-if 后来才渲染的子组件不在此列。
  • Vue 3 选项式 API 还能写 beforeDestroy 吗?不能直接使用,需要改名;迁移构建 @vue/compat 会对旧名给出兼容与警告。