Appearance
21|Vue 计算属性、侦听器与生命周期
响应式数据解决"数据变了页面自动更新"的问题,但页面中还有很多值不是直接的数据,而是数据的衍生结果:根据磁盘总量和已用量计算使用率、根据筛选条件过滤列表。计算属性(computed)缓存这些衍生结果;侦听器(watch)在数据变化时执行副作用;生命周期钩子则在组件的不同阶段执行初始化或清理逻辑。
一、计算属性 computed
计算属性基于现有响应式数据计算出新值,具有缓存机制:依赖的数据不变时,多次访问计算属性不会重新计算。
vue
<script setup>
import { ref, computed } from "vue";
const total = ref(100);
const used = ref(45);
// 计算属性
const usagePercent = computed(() => {
return Math.round((used.value / total.value) * 100);
});
// 计算属性返回对象
const status = computed(() => {
const p = usagePercent.value;
if (p >= 90) return { text: "严重", class: "danger" };
if (p >= 80) return { text: "警告", class: "warning" };
return { text: "正常", class: "ok" };
});
</script>
<template>
<div>
<p>使用率: {{ usagePercent }}%</p>
<p :class="status.class">状态: {{ status.text }}</p>
</div>
</template>computed vs 方法
模板中也可以调用方法,但方法每次渲染都会执行,没有缓存:
vue
<template>
<!-- 每次渲染都重新计算 -->
<p>{{ calcUsage() }}</p>
<!-- 依赖不变时直接读缓存 -->
<p>{{ usagePercent }}</p>
</template>如果计算开销大或依赖不常变,用 computed;如果只是简单的格式化且不需要缓存,用方法也可以。
可写计算属性
默认计算属性是只读的,也可以定义 setter:
js
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(" ");
},
});
fullName.value = "John Doe"; // 触发 setter,更新 firstName 和 lastName二、侦听器 watch
计算属性用于派生数据,侦听器用于执行副作用(如发送请求、记录日志、操作 DOM)。
监听 ref
vue
<script setup>
import { ref, watch } from "vue";
const searchQuery = ref("");
watch(searchQuery, (newValue, oldValue) => {
console.log(`搜索从 "${oldValue}" 变为 "${newValue}"`);
// 触发搜索请求
fetchResults(newValue);
});
</script>监听 reactive 对象的属性
js
const server = reactive({ hostname: "web-01", status: "running" });
// 监听单个属性
watch(() => server.status, (newVal, oldVal) => {
console.log(`状态从 ${oldVal} 变为 ${newVal}`);
});
// 监听整个对象(深监听)
watch(server, (newVal, oldVal) => {
console.log("server 发生变化");
}, { deep: true });监听 reactive 对象的整体变化时,新旧值是同一个对象引用(因为 Vue 不会克隆对象),比较时需要注意。
立即执行和一次性监听
js
// immediate: 初始化时立即执行一次回调
watch(searchQuery, (val) => {
fetchResults(val);
}, { immediate: true });
// once: 只触发一次(Vue 3.4+)
watch(source, callback, { once: true });watchEffect
watchEffect 自动追踪回调中用到的响应式数据,不需要显式指定监听源:
js
import { ref, watchEffect } from "vue";
const hostname = ref("web-01");
const port = ref(8080);
watchEffect(() => {
console.log(`当前服务器: ${hostname.value}:${port.value}`);
});
// 修改 hostname 或 port 都会触发上面的回调watchEffect 在初始化时立即执行一次。和 watch 的区别:
| watch | watchEffect | |
|---|---|---|
| 监听源 | 显式指定 | 自动追踪回调中用到的响应式数据 |
| 旧值 | 可以获取 | 无法获取 |
| 初始化 | 默认不执行(除非加 immediate) | 立即执行 |
三、生命周期钩子
组件从创建到销毁会经历一系列阶段,Vue 在每个阶段提供钩子函数:
创建阶段 挂载阶段 更新阶段 卸载阶段
setup() → onMounted() → onUpdated() → onUnmounted()onMounted
组件挂载到 DOM 后执行,适合初始化操作:
vue
<script setup>
import { ref, onMounted } from "vue";
const list = ref([]);
onMounted(async () => {
const response = await fetch("/api/servers");
list.value = await response.json();
});
</script>onUnmounted
组件从 DOM 移除前执行,适合清理工作:
vue
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const timer = ref(null);
onMounted(() => {
timer.value = setInterval(() => {
refreshData();
}, 5000);
});
onUnmounted(() => {
clearInterval(timer.value); // 清除定时器,防止内存泄漏
});
</script>常用生命周期钩子
| 钩子 | 触发时机 | 用途 |
|---|---|---|
onMounted | 组件挂载到 DOM 后 | 发送请求、操作 DOM、绑定事件 |
onUpdated | 数据更新导致 DOM 重新渲染后 | 依赖更新后的 DOM 状态 |
onUnmounted | 组件卸载前 | 清除定时器、取消请求、解绑事件 |
Vue 3 的 setup 语法糖中,onMounted 等钩子直接从 vue 导入使用。不需要像 Vue 2 那样写在 mounted() 选项中。
四、常见错误
在 computed 中修改数据
js
// 错误:计算属性应该是纯函数,不应该有副作用
const usage = computed(() => {
total.value = 100; // 副作用
return used.value / total.value;
});
// 正确:只读取,不修改
const usage = computed(() => used.value / total.value);watch 监听 reactive 对象忘记用 getter
js
const server = reactive({ status: "running" });
// 错误:直接传对象,监听的是对象引用变化,不是属性变化
watch(server, () => { /* 不会触发 */ });
// 正确:传 getter 函数
watch(() => server.status, () => { /* 正常触发 */ });忘记清除副作用
js
// 错误:组件卸载后定时器仍在运行
onMounted(() => {
setInterval(() => {
fetchData();
}, 5000);
});
// 正确:卸载时清除
let timer;
onMounted(() => {
timer = setInterval(fetchData, 5000);
});
onUnmounted(() => {
clearInterval(timer);
});