4. Vue3 核心基础(二):计算属性、侦听器与生命周期钩子
上一章我们聊了响应式数据和模板语法,算是把 Vue3 的「手脚」活动开了。但真正写业务逻辑时,你会发现光有 ref 和 reactive 还不够——数据要加工、变化要监听、组件什么时候挂载也得心里有数。这一章,我们就来啃这三块硬骨头。
本章核心脉络:计算属性解决「衍生状态」的声明式表达;侦听器处理「副作用」的精准触发;生命周期钩子让你在组件的关键时刻「插一脚」。三者配合,才能写出健壮、可维护的看板应用。
4.1 计算属性(computed)—— 别把逻辑写在模板里
先问个问题:你会在模板里写 {{ items.filter(i => i.price > 100).length }} 吗?我以前刚学 Vue 的时候就这么干过。结果模板又长又难读,而且每次渲染都要重新计算——性能浪费不说,维护起来也头疼。
计算属性就是来解决这个问题的。它本质上是一个「派生值」,基于已有的响应式数据自动计算,并且会缓存结果。只要依赖没变,它就返回缓存,不会重复执行。
我的习惯:只要模板里出现超过一个三元表达式或者链式调用,我就立刻抽成计算属性。这不仅是性能问题,更是代码可读性的底线。
<script setup>
import { ref, computed } from 'vue'
const searchQuery = ref('')
const metrics = ref([
{ name: 'CPU 使用率', value: 78, unit: '%' },
{ name: '内存占用', value: 4.2, unit: 'GB' },
{ name: '磁盘 IO', value: 256, unit: 'MB/s' }
])
// 计算属性:过滤后的指标列表
const filteredMetrics = computed(() => {
if (!searchQuery.value) return metrics.value
return metrics.value.filter(m => m.name.includes(searchQuery.value))
})
// 计算属性:统计高负载指标
const highLoadCount = computed(() => {
return metrics.value.filter(m => m.value > 80).length
})
</script>
注意,计算属性默认是只读的。你可能会想:「那我能不能直接给它赋值?」可以,但需要提供 setter。不过我在项目中很少用——计算属性的 setter 容易让数据流变得混乱。如果你发现需要给计算属性赋值,多半是设计上该用普通状态了。
避坑指南:计算属性里不要做异步操作或者修改 DOM。它必须是纯函数——输入确定,输出就确定。我曾经在计算属性里调了一个 API,结果页面卡死,排查了半天才发现是计算属性触发了无限循环。
4.2 侦听器(watch)—— 精准响应变化
计算属性适合「根据已有数据算出新数据」。但有些场景不一样——比如搜索框输入后要调接口、某个值变化后要写 localStorage、或者数据变化后要触发动画。这些「副作用」操作,就该交给侦听器。
Vue3 的 watch 比 Vue2 灵活了不少。你可以监听单个 ref、多个 ref,甚至是一个 getter 函数。我个人最常用的是监听一个计算属性,然后触发异步请求。
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const debounceTimer = ref(null)
// 监听单个 ref
watch(keyword, (newVal, oldVal) => {
console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
// 防抖处理
clearTimeout(debounceTimer.value)
debounceTimer.value = setTimeout(() => {
fetchSearchResults(newVal)
}, 300)
})
// 监听多个来源
const cpuUsage = ref(45)
const memoryUsage = ref(60)
watch([cpuUsage, memoryUsage], ([newCpu, newMem], [oldCpu, oldMem]) => {
if (newCpu > 90 || newMem > 90) {
triggerAlert('资源使用率过高')
}
}, { deep: true })
</script>
这里有个细节:deep: true 用于监听对象内部的变化。但要注意,深度监听开销不小。如果你只关心某个嵌套属性,不如用 getter 函数精确监听。
watch vs watchEffect:watchEffect 会自动收集依赖,不需要手动指定监听目标。适合「只要依赖变了就重新执行」的场景,比如日志记录、数据同步。但如果你需要获取旧值,或者需要懒执行(默认不立即执行),那就用 watch。
我记得有一次做实时看板,需要监听 WebSocket 推送的数据变化来更新图表。用 watch 配合 immediate: true,组件一创建就执行一次初始化,后续每次数据变化自动更新——代码干净,逻辑清晰。
4.3 生命周期钩子——在正确的时机做正确的事
组件不是凭空出现的。它要经历创建、挂载、更新、卸载这几个阶段。Vue3 提供了组合式 API 的生命周期钩子,让你在每个阶段「插一脚」。
最常用的几个:
| 钩子函数 | 触发时机 | 典型用途 |
|---|---|---|
onMounted |
组件挂载到 DOM 后 | 发起 API 请求、操作 DOM、初始化第三方库 |
onBeforeUpdate |
响应式数据变化,DOM 更新前 | 获取更新前的 DOM 状态 |
onUpdated |
DOM 更新完成后 | 执行依赖新 DOM 的操作 |
onUnmounted |
组件卸载前 | 清除定时器、取消订阅、释放资源 |
onActivated |
被 <KeepAlive> 缓存的组件激活时 |
恢复轮询、刷新数据 |
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const cpuData = ref([])
let pollingTimer = null
onMounted(() => {
// 初始化数据
fetchCpuData()
// 启动轮询
pollingTimer = setInterval(fetchCpuData, 5000)
})
onUnmounted(() => {
// 清理资源——这个很容易忘
if (pollingTimer) {
clearInterval(pollingTimer)
pollingTimer = null
}
})
function fetchCpuData() {
// 模拟 API 请求
cpuData.value.push({
time: Date.now(),
value: Math.random() * 100
})
}
</script>
我曾经踩过的坑:在 onMounted 里注册了全局事件监听,但忘了在 onUnmounted 里移除。结果组件切换了几次后,事件绑定了好几层,每次触发都执行多次回调。排查了半天才发现是事件泄漏。记住:谁注册,谁销毁。
另外,Vue3 的生命周期钩子只能在 setup 中同步调用。你不能在 setTimeout 或者异步回调里写 onMounted——那样会报错。原因很简单,Vue 需要在组件初始化时收集这些钩子,异步调用就收集不到了。
4.4 三者如何协作?一个真实场景
光讲理论没意思。我们来看一个实时看板中常见的场景:搜索过滤 + 数据轮询。
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
// 原始数据
const allMetrics = ref([])
// 搜索关键词
const searchText = ref('')
// 轮询间隔
const POLL_INTERVAL = 3000
// 计算属性:过滤后的数据
const displayMetrics = computed(() => {
if (!searchText.value) return allMetrics.value
return allMetrics.value.filter(m =>
m.name.toLowerCase().includes(searchText.value.toLowerCase())
)
})
// 侦听器:搜索词变化时打日志(方便调试)
watch(searchText, (val) => {
console.log(`搜索词变化: ${val},当前显示 ${displayMetrics.value.length} 条`)
})
// 生命周期:挂载时启动轮询,卸载时清理
let timer = null
onMounted(() => {
fetchData()
timer = setInterval(fetchData, POLL_INTERVAL)
})
onUnmounted(() => {
clearInterval(timer)
})
async function fetchData() {
// 模拟 API 请求
const res = await mockApi()
allMetrics.value = res
}
</script>
你看,这个模式在实时看板里非常典型:computed 负责数据加工,watch 负责副作用(日志、调试),onMounted 和 onUnmounted 管理生命周期。各司其职,互不干扰。
我的建议:写组件时先想清楚「哪些是状态」「哪些是派生值」「哪些是副作用」。状态用 ref/reactive,派生值用 computed,副作用用 watch 或生命周期。这个思维模型一旦建立,写出来的代码自然就清晰了。