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

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 中父子组件生命周期执行顺序回顾

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/39-Vue中父子组件生命周期执行顺序回顾
本文目录12 个章节
  1. 一、问题背景
  2. 二、首次加载和挂载顺序
  3. 三、更新顺序
  4. 四、卸载和销毁顺序
  5. 五、KeepAlive 对顺序的影响
  6. 六、父组件请求数据,子组件什么时候能拿到?
  7. 七、created 和 mounted 中请求的选择
  8. 八、生命周期中不要使用箭头函数绑定 Options API 的 this
  9. 九、常见生命周期顺序速查
  10. 十、总结
  11. 官方参考
  12. 原文出处

Vue 中父子组件生命周期执行顺序回顾

原文以 Vue 2 为主,结论是“父组件先创建,子组件先挂载”。这个主线在 Vue 3 的普通同步组件树中仍然成立,但生命周期名称、异步请求和 KeepAlive 的说明需要补充边界。

一、问题背景

在单一组件中,常见的生命周期顺序是:

beforeCreate -> created -> beforeMount -> mounted
-> beforeUpdate -> updated
-> beforeUnmount/unmounted(Vue 3)
-> beforeDestroy/destroyed(Vue 2)

当父组件渲染子组件时,父子组件各自拥有独立的生命周期。渲染器需要先创建父组件上下文,再创建子组件;挂载 DOM 时则通常先完成子组件,再认为父组件的整个子树完成挂载。

以下顺序指的是普通的、同步的、首次渲染的父子组件树。如果组件被 v-if、异步组件、SuspenseKeepAlive 或条件分支包裹,实际执行的钩子会受到这些机制影响。

二、首次加载和挂载顺序

Vue 2 / Vue 3 Options API 的常见顺序

父 beforeCreate
-> 父 created
-> 父 beforeMount
-> 子 beforeCreate
-> 子 created
-> 子 beforeMount
-> 子 mounted
-> 父 mounted

也可以简写为:

父创建 -> 子创建 -> 子挂载 -> 父挂载

父组件的 mounted 通常要等它的同步子组件也挂载完成后才执行,所以原文“子组件先挂载,父组件后挂载”的观察是有依据的。

Vue 3 Composition API 的对应观察

Composition API 没有 beforeCreatecreated 两个同名 hook;setup() 承担了组件创建阶段的组合式逻辑。常见日志关系可以理解为:

父 setup
-> 父 onBeforeMount
-> 子 setup
-> 子 onBeforeMount
-> 子 onMounted
-> 父 onMounted

setup() 中不能访问已经挂载的 DOM。需要 DOM 时使用 onMounted,需要等待本轮响应式更新后的 DOM 时使用 nextTick()

为什么父组件先创建、子组件先挂载?

  1. 创建父组件实例时,需要先解析父组件的 props、状态和渲染函数;
  2. 父组件 render 得到子组件 vnode 后,渲染器开始创建子组件实例;
  3. 子组件必须先完成自己的 render 和 DOM patch,父组件的子树才算完成;
  4. 因此父组件的 mounted/onMounted 放在子组件挂载之后执行。

这不是“父组件会等待所有网络请求”的意思。生命周期顺序只描述组件渲染阶段,不会等待任意 Promise。

三、更新顺序

当一次响应式更新同时导致父、子组件需要更新时,常见顺序是:

父 beforeUpdate
-> 子 beforeUpdate
-> 子 updated
-> 父 updated

Vue 3 的 Composition API 对应:

父 onBeforeUpdate
-> 子 onBeforeUpdate
-> 子 onUpdated
-> 父 onUpdated

但这不是每次数据变化都完整执行的固定脚本:

  • 只有真正需要更新的组件才会运行 update hook;
  • Vue 会批量合并同步发生的状态变化;
  • 父组件更新不一定意味着每个子组件也更新;
  • 一个组件的 updated 不保证能说明是哪一个属性发生了变化;
  • DOM 更新完成后仍然应使用 nextTick 处理依赖 DOM 的逻辑。
<script setup>
import { nextTick, ref } from 'vue'

const count = ref(0)

async function increment() {
  count.value++
  await nextTick()
  // 此时当前组件因 count 变化产生的 DOM 更新已经完成
}
</script>

四、卸载和销毁顺序

Vue 2 的名称是 beforeDestroy / destroyed;Vue 3 将它们改为 beforeUnmount / unmounted

父 beforeUnmount / beforeDestroy
-> 子 beforeUnmount / beforeDestroy
-> 子 unmounted / destroyed
-> 父 unmounted / destroyed

Vue 3 Composition API:

<script setup>
import { onBeforeUnmount, onUnmounted } from 'vue'

onBeforeUnmount(() => {
  console.log('组件即将卸载')
})

onUnmounted(() => {
  console.log('组件及其子树已经卸载')
})
</script>

在卸载前取消请求、移除全局事件、停止观察器;在卸载后不要再访问已经失效的组件 DOM。

五、KeepAlive 对顺序的影响

<KeepAlive> 缓存的组件切换离开时不会立即执行普通卸载流程,而是进入停用状态:

首次进入:created/setup -> mounted/onMounted -> activated/onActivated
切换离开:deactivated/onDeactivated
再次进入:activated/onActivated
真正淘汰或父级卸载:unmounted/onUnmounted

Vue 2 对应 activateddeactivated,Vue 3 Composition API 使用 onActivatedonDeactivated。因此不能因为路由离开就假设组件已经 destroyed/unmounted。

六、父组件请求数据,子组件什么时候能拿到?

原文中“父组件请求完成后,子组件在 created 或 mounted 中就一定能拿到接口数据”的说法是不准确的。

例如父组件在 createdonMounted 中发起异步请求:

父组件创建/挂载
-> 发起 fetch
-> 子组件按照当前 render 条件创建并挂载
-> fetch 未来某个时间完成
-> 父状态更新
-> 子组件收到新的 prop 并更新

请求不会阻塞 Vue 继续创建子组件。子组件第一次创建时,prop 可能是 undefined、空数组或默认值;请求返回后,父组件更新状态,子组件再收到新的 prop。

方案一:使用 loaded 标志决定是否创建子组件

如果子组件必须在创建时就拿到完整数据,可以让 v-if 控制组件是否创建:

<template>
  <div class="test">
    <ChildPanel
      v-if="loaded"
      :data="data1"
    />
    <p v-else>加载中……</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import ChildPanel from './ChildPanel.vue'

const loaded = ref(false)
const data1 = ref(null)

async function loadData() {
  data1.value = await fetch('/api/data').then(response => response.json())
  loaded.value = true
}

loadData()
</script>

不要简单用 v-if="data1" 代替 loaded,除非你能确定空对象、0、空字符串等都不可能是合法结果。更稳妥的是显式维护 loadedloadingerror 状态。

这种方式的特点是:请求完成前子组件根本不存在,所以不会执行子组件的 created/mounted;请求完成后才会按正常顺序创建子组件。

方案二:子组件接受初始值,并 watch prop 变化

如果子组件可以先显示 loading 或空状态,不需要延迟创建它,就让 prop 支持初始值,并监听变化:

Vue 2 Options API

export default {
  props: {
    data1: {
      type: Object,
      default: null,
    },
  },
  watch: {
    data1: {
      immediate: true,
      deep: true,
      handler(newValue) {
        if (newValue) {
          this.showData(newValue)
        }
      },
    },
  },
  methods: {
    showData(value) {
      // 使用 value 更新子组件自己的 data
    },
  },
}

Vue 3 Composition API

<script setup>
import { watch } from 'vue'

const props = defineProps({
  data1: {
    type: Object,
    default: null,
  },
})

watch(
  () => props.data1,
  value => {
    if (value) {
      showData(value)
    }
  },
  { immediate: true },
)

function showData(value) {
  // 使用 value 更新自己的状态
}
</script>

如果只需要显示数据,通常不需要复制到子组件的 data,直接使用 prop 或 computed 即可:

computed: {
  displayUrl() {
    return this.data1?.url || ''
  },
}

子组件不能写 this.data1 = newValprops.data1 = newVal,因为 prop 是父组件传入的只读数据。需要修改时,应 emit 事件通知父组件,或者把 prop 作为初始值复制到一个明确的本地状态中。

方案三:用 computed 派生展示数据

<script setup>
import { computed } from 'vue'

const props = defineProps({
  data1: Object,
})

const displayUrl = computed(() => props.data1?.url ?? '')
</script>

<template>
  <a v-if="displayUrl" :href="displayUrl">查看详情</a>
  <span v-else>暂无链接</span>
</template>

watch 适合执行副作用,例如重新请求、初始化第三方插件;computed 适合从 prop 推导出模板需要的值。不要为了“响应式”而在 watch 中把 prop 再赋值给自己。

七、created 和 mounted 中请求的选择

Vue 2

  • created:已经可以访问响应式 data、props、methods,但还没有挂载 DOM;适合不依赖 DOM 的数据请求。
  • mounted:组件 DOM 已经挂载;适合必须操作 DOM 的第三方库初始化。

Vue 3

  • setup() 中可以创建状态和请求逻辑;
  • onMounted 只用于依赖浏览器 DOM 的逻辑,不要因为“请求必须 mounted”而滥用;
  • SSR 应考虑 onServerPrefetch、框架的数据获取机制或在服务端路由中加载数据;
  • fetch 是异步的,放在 created、setup 或 mounted 都不会改变父子组件本身的同步挂载顺序。

八、生命周期中不要使用箭头函数绑定 Options API 的 this

Vue 2/3 Options API 会把生命周期函数的 this 绑定到组件实例,因此不要这样写:

export default {
  // ❌ 箭头函数没有自己的 this
  created: () => {
    this.fetchTodos()
  },
}

应使用普通函数:

export default {
  created() {
    this.fetchTodos()
  },
}

Composition API 不依赖组件实例 this,应直接使用闭包中的 refs、props 和 composables:

<script setup>
import { onMounted, ref } from 'vue'

const todos = ref([])

onMounted(async () => {
  todos.value = await fetch('/api/todos').then(response => response.json())
})
</script>

九、常见生命周期顺序速查

首次渲染

父 beforeCreate/created/beforeMount
-> 子 beforeCreate/created/beforeMount
-> 子 mounted
-> 父 mounted

更新

父 beforeUpdate
-> 子 beforeUpdate
-> 子 updated
-> 父 updated

卸载

父 beforeUnmount/beforeDestroy
-> 子 beforeUnmount/beforeDestroy
-> 子 unmounted/destroyed
-> 父 unmounted/destroyed

这些顺序是理解普通同步组件树的模型,不应覆盖真实渲染器的所有边界。异步组件、Suspense、Teleport、Transition、KeepAlive、条件分支和批量更新都可能改变观察结果。

十、总结

  • 普通首次渲染通常是父组件先创建,子组件先挂载,父组件最后 mounted。
  • 父子生命周期不会等待接口请求返回;请求完成后是一次新的响应式更新。
  • 子组件需要完整数据时,可以用 loaded 配合 v-if 延迟创建;也可以允许初始空值,再用 watch/computed 响应 prop 变化。
  • 子组件不能直接修改 prop,原文中在 watch 里给 this.data1 赋值的写法应删除。
  • Vue 2 使用 beforeDestroy/destroyed,Vue 3 使用 beforeUnmount/unmounted
  • KeepAlive 下离开组件通常是 deactivated,不是销毁;再次进入执行 activated
  • Options API 生命周期不要用箭头函数;Composition API 使用 setuponXxx hooks,不依赖 this

官方参考

原文出处

作者:希希118

原文链接:https://juejin.cn/post/6844904113914773518

来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS