理解什么是生命周期
Q: Vue 组件的「生命周期」指的是什么?钩子函数有什么作用?
A: 生命周期是组件从创建到最终销毁所经历的一系列过程。在这个过程中,Vue 设置了一些特殊的时间点,开发者可以在这些时间点注册自己的函数——这类函数被称为钩子函数(Hook)。必须等开发者注册的钩子函数执行完毕后,Vue 才会继续走后面的流程。这就给了开发者在特定阶段执行自己业务逻辑的机会。
Vue3 完整的生命周期图如下:
图片展示了 Vue3 组件从创建、挂载、更新到卸载的完整生命周期流程,以及各阶段对应的钩子函数。
快速入门:onMounted
虽然生命周期钩子函数有很多,但在实际开发中,有一个是最先需要掌握的——onMounted。
onMounted 会在组件完成了初始化渲染并且 DOM 节点已创建并插入之后被触发。这意味着在 onMounted 中,你可以安全地访问真实的 DOM 元素,也是请求后端数据的最佳时机。
下面是一个典型的使用场景——在组件挂载后请求用户列表:
<template>
<div>
<h1>用户列表</h1>
<ul v-if="!loading">
<li v-for="(user, index) in users" :key="index">
{{ user.name }} - {{ user.email }}
</li>
</ul>
<div v-if="loading">加载中...</div>
<div v-if="error">出错啦!</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const users = ref([]);
const loading = ref(false);
const error = ref(null);
onMounted(async () => {
// 一般来讲,我们会在此生命周期钩子方法中请求数据
loading.value = true;
try {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
if (res.ok) {
users.value = await res.json();
} else {
throw new Error('请求失败');
}
} catch (err) {
error.value = err.message;
} finally {
// 无论成功还是失败,都需要关闭 loading 状态
loading.value = false;
}
});
</script>
这个例子中包含了三个常见状态的管理——loading(加载中)、数据展示(成功)、error(失败),是真实项目中请求数据的标准范式。
Vue2 与 Vue3 的生命周期钩子差异
Vue3 是支持 Vue2 的选项式 API 写法的,所以两套生命周期方法在 Vue3 中可以共存,但命名有所不同:
| 生命周期阶段 | Vue3(组合式 API) | Vue2(选项式 API) |
|---|---|---|
| 挂载后 | onMounted |
mounted |
| 更新后 | onUpdated |
updated |
| 卸载前 | onBeforeUnmount |
beforeDestroy |
| 卸载后 | onUnmounted |
destroyed |
如果同一个生命周期阶段同时设置了两种形式的钩子方法,Vue3 的组合式钩子会先于 Vue2 的选项式钩子执行:
<template>
<div>Vue2 和 Vue3 钩子函数执行时机对比</div>
</template>
<script>
import { onMounted } from 'vue';
export default {
mounted() {
console.log('执行Vue2的钩子函数');
},
setup() {
onMounted(() => {
console.log('执行Vue3的onMounted函数');
});
},
};
</script>
运行结果:Vue3 的 onMounted 会比 Vue2 的 mounted 先输出。不过这一点知道即可——真实开发中绝对不会两个版本混着用。
侦听器(Watch)快速入门
Q: 侦听器和计算属性有什么区别?什么时候用侦听器?
A: 侦听器和计算属性类似,都是依赖响应式数据。区别在于:
- 计算属性:依赖数据变化时,重新做二次计算,不涉及副作用(如修改 DOM、发请求)。
- 侦听器:依赖数据变化时,允许执行副作用操作——更改 DOM、发送异步请求、操作本地存储等。
下面是一个经典的侦听器示例——智能机器人问答:
<template>
<div>
<h1>智能机器人</h1>
<div>
<input v-model="question" placeholder="请输入问题" />
</div>
<div v-if="loading">正在加载中...</div>
<div v-else>{{ answer }}</div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const question = ref(''); // 存储用户输入的问题,以 ? 结束
const answer = ref(''); // 存储机器人的回答
const loading = ref(false);
// 侦听 question 的变化
watch(question, async (newValue) => {
if (newValue.includes('?')) {
loading.value = true;
answer.value = '思考中....';
try {
const res = await fetch('https://yesno.wtf/api');
const result = await res.json();
answer.value = result.answer;
} catch (err) {
answer.value = '抱歉,我无法回答您的问题';
} finally {
loading.value = false;
}
}
});
</script>
在这个例子中,watch 侦听 question 这个 ref 的变化,回调函数接收两个参数:新的状态值和旧的状态值。当用户输入包含 ? 时,触发异步请求获取答案——这就是典型的副作用操作。
侦听的数据源类型
watch 的第一个参数(数据源)支持多种类型:
1. 侦听 ref: 上面已经演示。
2. 侦听计算属性:
<template>
<div>
<input type="text" v-model="firstName" placeholder="first name" />
<input type="text" v-model="lastName" placeholder="last name" />
<p>全名:{{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
watch(fullName, (newVal, oldVal) => {
console.log(`new: ${newVal}, old: ${oldVal}`);
});
</script>
3. 侦听 reactive 响应式对象:
<template>
<div>
<input type="text" v-model="user.name" placeholder="name" />
<input type="text" v-model="user.age" placeholder="age" />
<p>用户信息:{{ user.name }} - {{ user.age }}</p>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue';
const user = reactive({
name: 'John',
age: 18,
});
watch(user, () => {
console.log('触发了侦听器回调函数');
});
</script>
4. 侦听 Getter 函数(函数的返回值):
<template>
<div>
<input type="number" v-model="count" />
<p>是否为偶数?{{ isEven() }}</p>
<div>count2: {{ count2 }}</div>
<button @click="count2++">+1</button>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const count = ref(0);
const count2 = ref(0);
function isEven() {
console.log('isEvent 函数被重新执行了');
if (count2.value === 5) return 'this is a test';
return count.value % 2 === 0;
}
// 侦听 isEven 函数的返回值
watch(isEven, () => {
console.log('触发了侦听器回调函数');
});
</script>
这里侦听的是 isEven() 这个函数的返回值结果——只有当返回值发生变化时,才会触发回调。
5. 侦听多个数据源组成的数组:
<template>
<div>
<div><input type="text" v-model="title" /></div>
<div><textarea v-model="description" cols="30" rows="10"></textarea></div>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const title = ref('');
const description = ref('');
// 数组中任何一个数据发生变化,都会触发回调函数
watch([title, description], () => {
console.log('侦听器的回调函数执行了');
});
</script>
这种方式非常适合需要同时监控多个输入的场景,比如表单的实时校验、自动保存等。
侦听层次与 deep 选项
Q: 侦听 reactive 对象时,嵌套属性变化会触发吗?
A: 当侦听的数据源是 reactive 类型时,默认就是深层次侦听——哪怕嵌套很深的对象属性发生变化,侦听器的回调也会执行。
<template>
<div>
<h1>任务列表</h1>
<ul>
<li v-for="task in tasks.list" :key="task.id">
{{ task.title }} - {{ task.completed ? '已完成' : '未完成' }}
<button @click="task.completed = !task.completed">切换状态</button>
</li>
</ul>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue';
const tasks = reactive({
list: [
{ id: 1, title: 'Task 1', completed: false },
{ id: 2, title: 'Task 2', completed: true },
],
});
watch(tasks, () => {
console.log('侦听器触发了!');
});
</script>
点击「切换状态」按钮修改 tasks.list[0].completed,虽然这是嵌套属性,侦听器依然会被触发。
注意:深层侦听虽然方便,但遍历大型数据结构的开销很大,只在必要时使用。
另外,侦听 reactive 对象时不能直接侦听其属性值:
const obj = reactive({ count: 0 });
// ❌ 错误:watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`);
});
需要改为 Getter 函数的形式:
const obj = reactive({ count: 0 });
// ✅ 正确:使用 Getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`);
},
);
第三个参数:配置选项
watch 接收三个参数:
- 第一个参数:侦听的数据源
- 第二个参数:数据变化时的回调函数
- 第三个参数:选项对象
关键配置项:
| 选项 | 类型 | 说明 |
|---|---|---|
immediate |
boolean | 默认回调是懒执行的;设为 true 后立即执行一次(常用于请求初始数据) |
once |
boolean | 回调只执行一次 |
deep |
boolean | 强制深层次侦听(主要针对计算属性/Getter 返回的对象,因为它们默认非深层) |
deep 选项的一个典型场景:侦听由计算属性或 Getter 函数返回的对象时,默认不是深层侦听,需要通过 deep: true 强制开启:
<script setup>
import { reactive, computed, watch } from 'vue';
const tasks = reactive([
{ id: 1, title: 'Learn Vue', completed: false },
{ id: 2, title: 'Read Documentation', completed: false },
{ id: 3, title: 'Build Something Awesome', completed: false },
]);
const selectedId = reactive({ id: null });
const selectedTask = computed(() => {
return tasks.find((task) => task.id === selectedId.id);
});
// 侦听 Getter 函数返回的计算属性值
// deep: true 确保嵌套属性变化也能触发
watch(
() => selectedTask.value,
() => {
console.log('Task details changed');
},
{ deep: true },
);
</script>
watchEffect:自动跟踪依赖
Q: watchEffect 和 watch 有什么区别?什么时候用哪个?
A: watchEffect 简化了 watch 的写法——不再需要显式指定响应式数据依赖。回调函数中用到了哪个响应式数据,哪个数据就会自动成为依赖。
对比写法:
watch:
const todoId = ref(1);
const data = ref(null);
watch(
todoId, // 需要显式指定依赖
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
);
data.value = await response.json();
},
{ immediate: true },
);
watchEffect:
// 不需要显式指定依赖,回调中用到 todoId 就自动追踪
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`,
);
data.value = await response.json();
});
对于单个依赖的场景,watchEffect 的收益不大。但当涉及多个依赖项时,watchEffect 的优势就体现出来了——你不需要手动罗列所有依赖。
另外,watchEffect 侦听嵌套数据结构时,只会侦听回调中实际用到的属性,而不是递归侦听所有属性:
<script setup>
import { reactive, watchEffect } from 'vue';
const team = reactive({
members: [
{ id: 1, name: 'Alice', role: 'Leader', status: 'Active' },
{ id: 2, name: 'Bob', role: 'Member', status: 'Inactive' },
],
});
function updateLeaderStatus() {
const leader = team.members.find((me) => me.role === 'Leader');
leader.status = leader.status === 'Active' ? 'Inactive' : 'Active';
}
function updateMemberStatus() {
const member = team.members.find((member) => member.role === 'Member');
member.status = member.status === 'Active' ? 'Inactive' : 'Active';
}
watchEffect(() => {
// 只关注 leader 的状态
const leader = team.members.find((me) => me.role === 'Leader');
console.log('Leader状态:', leader.status);
// 修改 Member 的状态不会触发此侦听器
});
</script>
这种精确追踪的特性在大型数据结构中能显著提升性能。
回调触发时机:flush 选项
默认情况下,侦听器回调在父组件更新之后、所属组件的 DOM 更新之前被调用。这意味着在回调中访问所属组件的 DOM 时,拿到的是 DOM 更新之前的状态。
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<div v-if="isShow" ref="divRef">
<p>this is a test</p>
</div>
<p>上面的高度为:{{ height }} pixels</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const isShow = ref(false);
const height = ref(0);
const divRef = ref(null);
watch(isShow, () => {
// 此时拿到的是 DOM 更新前的高度(0)
height.value = divRef.value ? divRef.value.offsetHeight : 0;
console.log(`当前获取的高度为:${height.value}`);
});
</script>
如果需要在 DOM 更新之后再执行回调,设置 flush: 'post' 即可:
watch(
isShow,
() => {
height.value = divRef.value ? divRef.value.offsetHeight : 0;
console.log(`当前获取的高度为:${height.value}`);
},
{ flush: 'post' },
);
停止侦听器
大多数情况下不需要关心停止侦听器——组件卸载时会自动停止组件上设置的侦听器。
但是这个自动停止仅限于同步设置的侦听器。如果是异步设置的侦听器,组件卸载也不会自动停止:
<script setup>
import { watchEffect } from 'vue';
// ✅ 自动停止
watchEffect(() => {});
// ❌ 不会自动停止!
setTimeout(() => {
watchEffect(() => {});
}, 100);
</script>
这种情况下需要手动停止。watch/watchEffect 的返回值是一个停止函数,调用它即可:
const unwatch = watchEffect(() => {});
// 手动停止
unwatch();
一个实际场景——当值达到某个条件后停止侦听:
<template>
<div>
<button @click="a++">+1</button>
<p>当前 a 的值为:{{ a }}</p>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const a = ref(1);
const message = ref('');
const unwatch = watch(
a,
(newVal) => {
if (newVal > 5) {
unwatch(); // 超过 5 后停止侦听
}
message.value = `当前 a 的值为:${a.value}`;
},
{ immediate: true },
);
</script>
小结
本文系统梳理了 Vue3 的生命周期与侦听器两大核心机制:
- 生命周期:组件从创建到销毁的过程,钩子函数让开发者在特定阶段插入逻辑。
onMounted是请求数据、操作 DOM 的最常用钩子。Vue3 组合式钩子优先于 Vue2 选项式钩子执行,但不要混用。 - watch 侦听器:支持 ref、reactive、计算属性、Getter 函数、多源数组等多种数据源。reactive 默认深层侦听(性能开销大),侦听属性值需使用 Getter 函数。通过
immediate、once、deep、flush选项精细控制行为。 - watchEffect:自动追踪回调中的响应式依赖,适合多依赖场景,且仅追踪实际用到的属性(相比 watch 的深层递归更高效)。
- 停止侦听:同步侦听器自动停止;异步侦听器需手动调用返回的停止函数。
掌握生命周期和侦听器,意味着你能够精确控制在「什么时间点」执行「什么逻辑」——这是构建复杂 Vue 应用的基础能力。