Skip to content

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 的区别:

watchwatchEffect
监听源显式指定自动追踪回调中用到的响应式数据
旧值可以获取无法获取
初始化默认不执行(除非加 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);
});