Vue3 生命周期与组件通信深度解析

Vue 组件的生命周期是其核心概念之一,它描述了一个组件从创建、挂载、更新到销毁的完整过程。理解生命周期对于编写可预测、高性能的 Vue 应用至关重要。本文将深入探讨 Vue3 的生命周期,并结合组件间通信(Props、Emits、Provide/Inject)来展示它们在实际开发中的协同工作方式。

1. 什么是生命周期?

想象一个组件就像一个有生命的物体:

  • 创建 (Creation):它在代码中被定义,初始化其数据和状态。
  • 挂载 (Mounting):它被渲染到浏览器的 DOM 中,用户可以看到它。
  • 更新 (Updating):当它的内部数据或外部传入的 props 发生变化时,它会重新渲染,以反映最新的状态。
  • 销毁 (Unmounting):它从 DOM 中被移除,清理所有占用的资源。

生命周期钩子(Lifecycle Hooks)就是在这些关键时间点被自动调用的函数,让开发者有机会在组件的不同阶段执行自定义逻辑。

2. Vue3 中的生命周期钩子

Vue3 提供了两种使用生命周期钩子的方式:选项式 API (Options API)组合式 API (Composition API)

2.1 选项式 API 生命周期

如果你习惯 Vue2 的写法,选项式 API 会非常亲切。你可以直接在组件的选项对象中定义钩子函数。

vue 复制代码
<script>
export default {
  name: 'LifecycleDemo',
  data() {
    return {
      message: 'Hello, Vue!'
    }
  },
  // 1. 创建阶段
  beforeCreate() {
    console.log('beforeCreate: 实例已初始化,但数据观测和事件/侦听器尚未设置。');
    console.log('此时 this.message:', this.message); // undefined
  },
  created() {
    console.log('created: 实例创建完成。数据观测、计算属性、方法、事件/侦听器已设置。');
    console.log('此时 this.message:', this.message); // 'Hello, Vue!'
    // 可以在这里发起异步请求,但不能访问 DOM
  },

  // 2. 挂载阶段
  beforeMount() {
    console.log('beforeMount: 模板编译/渲染函数已生成,但尚未挂载到 DOM。');
    console.log('此时 $el:', this.$el); // undefined
  },
  mounted() {
    console.log('mounted: 组件已挂载到 DOM。');
    console.log('此时 $el:', this.$el); // <div ...>...</div>
    // **非常重要的钩子**,可以在这里进行 DOM 操作、初始化非 Vue 插件等
  },

  // 3. 更新阶段
  beforeUpdate() {
    console.log('beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。');
    // 适合在更新前访问现有的 DOM,比如手动移除已添加的事件监听器
  },
  updated() {
    console.log('updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁完成。');
    // DOM 已经更新完毕
    // **注意**:不要在此钩子中修改数据,否则可能导致无限循环的更新!
  },

  // 4. 销毁阶段
  beforeUnmount() {
    console.log('beforeUnmount: 组件实例销毁之前。');
    // 实例仍然完全可用。适合在这里清理定时器、解绑全局事件、销毁第三方库实例等。
  },
  unmounted() {
    console.log('unmounted: 组件实例销毁之后。');
    // 所有指令的绑定、事件监听器和子组件实例都已被移除。
  }
}
</script>

2.2 组合式 API 生命周期

组合式 API 提供了更灵活的代码组织方式。它将生命周期钩子作为独立的函数从 vue 包中导入。

选项式 API 组合式 API (钩子) 描述
beforeCreate - setup() 中,逻辑会自动执行于此阶段。
created - setup() 中,逻辑会自动执行于此阶段。
beforeMount onBeforeMount 组件挂载前调用。
mounted onMounted 组件挂载后调用。
beforeUpdate onBeforeUpdate 组件更新前调用。
updated onUpdated 组件更新后调用。
beforeUnmount onBeforeUnmount 组件卸载前调用。
unmounted onUnmounted 组件卸载后调用。

使用示例:

vue 复制代码
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue';

const message = ref('Hello, Vue!');

console.log('setup: 相当于 beforeCreate 和 created 的组合。');

onBeforeMount(() => {
  console.log('onBeforeMount: 组件即将挂载。');
});

onMounted(() => {
  console.log('onMounted: 组件已挂载。');
  // 适合进行 DOM 操作
});

onBeforeUpdate(() => {
  console.log('onBeforeUpdate: 组件即将更新。');
});

onUpdated(() => {
  console.log('onUpdated: 组件已更新。');
});

onBeforeUnmount(() => {
  console.log('onBeforeUnmount: 组件即将卸载。');
  // 适合清理工作
});

onUnmounted(() => {
  console.log('onUnmounted: 组件已卸载。');
});
</script>

为什么 setup() 取代了 beforeCreatecreated 因为 setup() 是在组件实例创建之初、所有选项式 API 钩子之前执行的,所以它天然地包含了 beforeCreatecreated 的功能。你所有在这两个钩子中想做的事(如数据初始化、发起 API 请求)都应该直接写在 setup() 中或其顶层。

3. 生命周期执行顺序:父子组件

当组件嵌套时,它们的生命周期钩子会按特定顺序执行。这对于理解数据流和组件交互至关重要。

挂载 (Mounting) 顺序:

  1. 父组件 beforeCreate
  2. 父组件 created
  3. 父组件 beforeMount
  4. 子组件 beforeCreate
  5. 子组件 created
  6. 子组件 beforeMount
  7. 子组件 mounted
  8. 父组件 mounted

结论: 父组件先准备好,然后子组件完成挂载,最后父组件才宣告挂载完成。这确保了父组件可以在 mounted 钩子中安全地访问已挂载的子组件。

更新 (Updating) 顺序:

  1. 父组件 beforeUpdate
  2. 子组件 beforeUpdate
  3. 子组件 updated
  4. 父组件 updated

结论: 父组件先进入更新准备,子组件完成更新后,父组件才完成自己的更新。

卸载 (Unmounting) 顺序:

  1. 父组件 beforeUnmount
  2. 子组件 beforeUnmount
  3. 子组件 unmounted
  4. 父组件 unmounted

结论: 父组件先准备卸载,然后子组件被完全卸载,最后父组件才完成卸载。这给了父组件在 beforeUnmount 中处理子组件相关逻辑的机会。

4. 组件通信与生命周期的结合

组件通信是 Vue 应用的基石,而生命周期则决定了在何时进行通信是安全和有效的。

4.1 父传子 (Props)

父组件通过 props 将数据传递给子组件。

关键点: 子组件的 propscreatedbeforeMountmounted 钩子中都是可用的。

场景: 子组件需要根据父组件传入的 id 来从服务器加载数据。

父组件 (Parent.vue)

vue 复制代码
<template>
  <ChildComponent :user-id="userId" />
</template>

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

const userId = ref(123);
</script>

子组件 (ChildComponent.vue)

vue 复制代码
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
  userId: {
    type: Number,
    required: true
  }
});

const userData = ref(null);

// 当组件挂载后,使用 props 中的 userId 发起请求
onMounted(async () => {
  console.log('子组件 mounted, userId is:', props.userId); // 123
  try {
    const response = await fetch(`/api/users/${props.userId}`);
    userData.value = await response.json();
  } catch (error) {
    console.error('Failed to fetch user data:', error);
  }
});
</script>

分析: 在子组件的 onMounted 钩子中,我们可以确信 props.userId 已经被父组件传入并可用,因此在这里发起 API 请求是安全的。

4.2 子传父 (Emits)

子组件通过 emits 触发事件,父组件监听这些事件来接收数据。

关键点: 子组件可以在任何生命周期钩子中触发事件,但通常是在某个用户交互或异步操作完成后。

场景: 子组件有一个按钮,点击后通知父组件更新一个值。

子组件 (ChildComponent.vue)

vue 复制代码
<template>
  <button @click="handleClick">Click Me</button>
</template>

<script setup>
import { defineEmits } from 'vue';

const emit = defineEmits(['child-click']);

const handleClick = () => {
  console.log('子组件按钮被点击,准备触发事件。');
  emit('child-click', 'Hello from Child!');
};
</script>

父组件 (Parent.vue)

vue 复制代码
<template>
  <div>
    <p>Message from child: {{ messageFromChild }}</p>
    <ChildComponent @child-click="handleChildClick" />
  </div>
</template>

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

const messageFromChild = ref('');

const handleChildClick = (message) => {
  console.log('父组件收到子组件事件:', message);
  messageFromChild.value = message;
};
</script>

分析: 事件的触发和监听是即时的,与特定的生命周期钩子没有强绑定。但父组件对事件的响应(如更新自身数据)会触发父组件的 beforeUpdateupdated 钩子。

4.3 跨层级通信 (Provide / Inject)

Provide / Inject 用于解决深层嵌套组件间的通信问题,避免了 props 的"层层透传"。

关键点: provide 的数据在子组件的 setupcreatedmounted 等钩子中都可以通过 inject 访问到。

场景: 祖父组件提供一个主题色,孙子组件注入并使用它。

祖父组件 (Grandparent.vue)

vue 复制代码
<script setup>
import { provide, ref } from 'vue';

const theme = ref('dark');

// 在 setup 中提供数据
provide('appTheme', theme);
</script>

孙子组件 (Grandchild.vue)

vue 复制代码
<script setup>
import { inject, onMounted } from 'vue';

// 在 setup 中注入数据
const theme = inject('appTheme');

onMounted(() => {
  console.log('孙子组件 mounted, theme is:', theme.value); // 'dark'
  // 可以根据 theme 值来设置 DOM 样式
});
</script>

分析: inject 可以在 setup 的任何地方使用,包括生命周期钩子。这使得深层组件能够轻松地访问祖先组件提供的数据。

5. 总结与最佳实践

  1. onMounted 是进行 DOM 操作和初始化外部库的首选位置。 此时组件已经被渲染到页面上。
  2. onBeforeUnmount 是进行清理工作的最佳位置。 务必在这里清除定时器 (clearInterval, clearTimeout)、解绑全局事件监听器 (window.removeEventListener)、销毁第三方库实例等,以防止内存泄漏。
  3. 避免在 updated 中修改数据。 这极易导致无限循环的更新。如果需要在 DOM 更新后执行某些操作,可以使用 nextTick
  4. 理解父子组件的生命周期顺序。 这对于处理依赖于子组件状态的父组件逻辑至关重要。例如,父组件想在 mounted 中调用子组件的方法,必须确保子组件已经 mounted
  5. setup() 是数据初始化和发起异步请求的好地方。 因为它在所有生命周期钩子之前执行,你可以在这里为组件的整个生命周期准备好响应式数据。
  6. 组件通信与生命周期的结合是自然而然的。 Props 和 Provide 的数据在组件的大部分生命周期中都是可用的,而 Emits 则可以在任何需要通知父级的时刻被触发。
相关推荐
拉不动的猪2 小时前
回顾关于筛选时的隐式返回和显示返回
前端·javascript·面试
yinuo2 小时前
不写一行JS!纯CSS如何读取HTML属性实现Tooltip
前端
gnip3 小时前
脚本加载失败重试机制
前端·javascript
遗憾随她而去.3 小时前
Uni-App 页面跳转监控实战:快速定位路由问题
前端·网络·uni-app
码农学院3 小时前
MSSQL字段去掉excel复制过来的换行符
前端·数据库·sqlserver
颜酱4 小时前
实现一个mini编译器,来感受编译器的各个流程
前端·javascript·编译器
妄小闲4 小时前
网页源代码 企业网站源码 html源码网站
前端·html
Ares-Wang4 小时前
Vue3》》 ref 获取子组件实例 原理
javascript·vue.js·typescript