一、生命周期钩子(组合式 API)
1.1 钩子总览
| 组合式钩子 | 对应选项式 | 触发时机 | 典型业务 |
|---|---|---|---|
onBeforeMount |
beforeMount | 挂载前 | 极少用 |
onMounted |
mounted | 挂载完成,DOM 可访问 | 操作 DOM、初始化图表 |
onBeforeUpdate |
beforeUpdate | 数据变化、DOM 更新前 | 更新前处理 |
onUpdated |
updated | DOM 更新后 | 依赖 DOM 的操作(慎用) |
onBeforeUnmount |
beforeDestroy | 卸载前 | 清理定时器、解绑事件 |
onUnmounted |
destroyed | 卸载后 | 收尾 |
关键变化 :组合式 API 没有 beforeCreate / created ------
setup本身就在创建阶段执行,作用等同二者。钩子必须在setup顶层同步调用。
vue
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from "vue";
const list = ref([]);
let timer: number | undefined;
onMounted(async () => {
list.value = await fetchList(); // 请求数据
timer = setInterval(() => {}, 1000); // 启动定时器
});
onBeforeUnmount(() => {
clearInterval(timer); // 必须清理,防内存泄漏
});
</script>
记忆口诀:onMounted 动 DOM 拉数据、onBeforeUnmount 清资源 。
keep-alive缓存组件还有onActivated/onDeactivated。
二、父传子:props
2.1 defineProps 声明与类型校验
vue
<!-- 子组件 Child.vue -->
<script setup lang="ts">
// 类型声明(编译期校验 + IDE 提示)
interface User {
name: string;
age: number;
}
// 方式一:纯 TS 类型
const props = defineProps<{
title: string;
user: User;
count?: number; // 可选
}>();
// 方式二:运行时校验 + 默认值(withDefaults)
const props2 = withDefaults(
defineProps<{ count?: number; list: string[] }>(),
{
count: 0,
list: () => [], // 对象/数组默认值用函数返回
}
);
</script>
<template>
<h3>{{ title }}</h3>
<p>{{ user.name }}</p>
</template>
vue
<!-- 父组件 -->
<child title="标题" :user="userInfo" :count="5" />
2.2 单向数据流
props 是只读的 ,子组件不能修改(修改会告警且不生效)。要派生值用 computed,要改父数据用 $emit / defineModel。
三、子传父:defineEmits 自定义事件
vue
<!-- 子组件 -->
<script setup lang="ts">
const emit = defineEmits<{
change: [value: number]; // 事件名 + 参数类型
submit: [payload: { name: string }];
}>();
function handleClick() {
emit("change", 42); // 触发事件并传参
}
</script>
<template>
<button @click="handleClick">上报</button>
</template>
vue
<!-- 父组件 -->
<script setup lang="ts">
function handleChange(val: number) {
console.log("子组件上报:", val);
}
</script>
<template>
<child @change="handleChange" />
</template>
四、组件双向绑定 defineModel(Vue 3.4+)
4.1 基本用法
vue
<!-- 子组件:可双向绑定的输入框 -->
<script setup lang="ts">
const model = defineModel<string>(); // 声明双向绑定值
function clear() {
model.value = "";
}
</script>
<template>
<input v-model="model" placeholder="输入内容" />
</template>
vue
<!-- 父组件 -->
<script setup lang="ts">
import { ref } from "vue";
const keyword = ref("");
</script>
<template>
<my-input v-model="keyword" />
</template>
4.2 多个 v-model 与默认值
ts
const visible = defineModel<boolean>("visible", { default: false }); // v-model:visible
const title = defineModel<string>("title"); // v-model:title
底层等价于
modelValue+update:modelValue(或具名xxx+update:xxx),Vue 3.4 起用defineModel一行搞定。
五、跨层级 provide / inject
解决祖先 → 任意后代的通信,免去逐层传 props。
ts
// 祖先组件
import { ref, provide } from "vue";
const theme = ref("light");
const updateTheme = (val: string) => (theme.value = val);
provide("theme", theme); // 提供响应式数据
provide("updateTheme", updateTheme); // 提供修改函数(避免后代直接改)
ts
// 后代组件(任意层级)
import { inject } from "vue";
const theme = inject<Ref<string>>("theme", ref("light")); // 第二个参数是默认值
const updateTheme = inject<(v: string) => void>("updateTheme", () => {});
| 要点 | 说明 |
|---|---|
| 响应式 | 提供 ref(或 reactive + toRefs),不要提供普通值(会失去响应) |
| 修改方式 | 同时 provide 修改函数,遵循单向数据流 |
| 键冲突 | 用 Symbol 或字符串常量统一管理 |
六、踩坑:props 只读、类型校验失效
| 坑 | 现象 | 解决 |
|---|---|---|
| 直接修改 props | 告警 + 修改不生效 | computed 派生 / defineModel / emit |
| props 类型校验"失效" | 运行时无报错(TS 类型是编译期) | 需要运行时校验用 withDefaults + 自定义 validator,或 defineProps 对象写法 |
| defineModel 不生效 | 报"没有此宏" | 升级 Vue 到 3.4+ |
| provide 普通值 | 后代修改不同步 | 提供 ref 并传修改函数 |
| inject 拿不到 | 父级未 provide 或键不一致 | 检查键名 + 提供默认值兜底 |
| emit 事件名大小写 | @myEvent 与 my-event 不匹配 |
统一用 kebab-case(my-event) |
七、总结
- 生命周期:6 个组合式钩子,setup 取代 beforeCreate/created,卸载前必清理;
- 父传子 :
defineProps+ TS 类型 /withDefaults默认值,只读单向流; - 子传父 :
defineEmits类型化声明 +$emit传参; - 双向绑定 :
defineModel(3.4+)一行实现,支持多个与默认值; - 跨层 :
provide+inject(传 ref + 修改函数,保持单向流); - 踩坑:props 只读、运行时校验、3.4 版本要求、事件命名 kebab-case。