六维教程

Vue3 计算与侦听

有了 refreactive,数据会响应式更新了。但光会更新还不够,数据之间往往还有两种”联动”需求:

  • 根据已有数据算出新数据:购物车总价、筛选后的列表、折扣价。
  • 数据变化时去做点事:保存草稿、发起搜索请求、更新页面标题——这类”连锁反应”。

在Vue3里,computed 管”算”,watch 管”指定数据一变就做事”,watchEffect 管”不指定,用到的数据变了就自动重跑”。

先把三个 API 的基本形态摆出来,混个眼熟,正文再逐个展开:

import { computed, watch, watchEffect } from 'vue'

// computed:根据已有数据算出新值,返回一个只读 ref
computed(() => price.value * count.value)

// watch:点名监听某个数据,一变就执行回调
watch(keyword, (newVal, oldVal) => { /* 变化后的处理 */ })

// watchEffect:不点名,回调里用到哪个数据,就自动听哪个
watchEffect(() => { /* 同步副作用 */ })

三者都是”丢一个函数进去”,区别全在依赖上:computed 靠读数据自动收集依赖(还能缓存结果);watch 靠手动点名;watchEffect 不点名,用了谁就听谁。

computed

基本用法

它接收一个”纯计算函数”,返回一个只读的 ref

特点是自动追踪依赖、结果会被缓存,依赖没变,访问多少次都直接用缓存,不会重复计算。

<script setup>
import { ref, computed } from 'vue'

const price = ref(89)
const count = ref(2)

// 总价:依赖 price 和 count,任何一个变了才会重新计算
const total = computed(() => price.value * count.value)
</script>

<template>
  <p>单价 {{ price }},数量 {{ count }}</p>
  <p>总价:{{ total }}</p>
</template>

几点细节:

  • total 也是 ref,脚本中用 total.value 读,模板直接写 total
  • computed 内做的是同步计算,不要在里面发请求、改别的数据(那是副作用,想办法留给 watch)。

为什么需要 computed

模板里直接写函数调用行不行?为什么还要 computed

试试用函数:定义一个 total 函数,模板里 {{ total() }} 调用,结果一样能显示:

const total = () => price.value * count.value
<p>总价:{{ total() }}</p>

但每次模板重新渲染都会重新调用这个函数,没有缓存;computed 在依赖不变时不重算,列表大、计算量大时,差距明显。

可写 computed

上面的 total只读的——它由别的数据派生,你只能读它,不能改它。可现实中经常要”既能展示、又能修改”的派生值。

举个最常见的例子:有一个 first(名)和一个 last(姓)两个字段,但输入框里想只显示一个合并的”全名”。先用 computed 拼出来:

<script setup>
import { ref, computed } from 'vue'

const first = ref('张')
const last = ref('三')

// 合并成全名 —— 只能读,不能写
const fullName = computed(() => first.value + ' ' + last.value)
</script>

<template>
  <input v-model="fullName" placeholder="编辑全名" />
</template>

模板里 v-model="fullName" 要做两件事:

  • 显示时 fullName
  • 输入时把新值写回 fullName

可上面的 computed 只有”读”(get)没有”写”的入口,所以用户一打字写回就报错:Write operation failed: computed value is readonly

要能回填数据,就得给 computed 配上一个 set,把输入的新全名拆回 firstlast,把”只读”升级成”可读写”:

const fullName = computed({
  // 读:怎么把 first + last 拼成全名 —— 用于展示
  get: () => first.value + ' ' + last.value,
  // 写:用户改了全名,怎么拆回去更新 first 和 last —— 由你决定
  set: (v) => {
    const parts = v.split(' ')
    first.value = parts[0]
    last.value = parts[1]
  }
})

流程是:显示时走 get 拼接全名,用户输入新全名 → set 收到新值拆开写回 firstlast → 底层数据变了,get 又自动重新拼接,展示同步更新。

这样 first / last 依然是唯一的数据源fullName 只是它们的一个”代理壳”,不会出现两处数据对不上的情况。

watch

watch 用来”监听某个数据,变了就去执行一段逻辑”。

const user = ref({ name: '小明', age: 18 })

watch(user, (newVal) => {
  console.log('用户信息变化', newVal)
}, { deep: true, immediate: true })

基本语法:watch(监听来源, 回调, 选项?),三个参数各管一件事:

  • 监听来源:监听谁。单个 ref、多个 ref 组成的数组,或一个”返回值的函数”(getter,如 () => user.value.age)。
  • 回调:来源一变就执行。回调会自动收到两个参数——新值 newVal旧值 oldVal,你可以直接用,也可以不接。监听多个来源时收到的是数组,如 ([newK, newC]) => ...
  • 选项immediatedeep 等配置放第三个参数,见下面的表格。

常用配置项:

选项 作用
immediate: true 监听建立时立即执行一次回调(常用于”初始也加载数据”)
deep: true 对象属性深层变化也触发(ref({...}) 默认只观察引用变化)

最典型的就是在数据变化后发请求、保存缓存、做 DOM 操作

<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')

// 每当 keyword 变化,自动保存一次搜索词
watch(keyword, (newVal, oldVal) => {
  console.log('搜索词从', oldVal, '变为', newVal)
  localStorage.setItem('keyword', newVal)
})
</script>

<template>
  <input v-model="keyword" placeholder="输入搜索词" />
</template>

还可以一次监听多个来源(数组形式),或用 getter 监听某个具体属性:

// 监听多个
watch([keyword, count], ([newK, newC]) => {
  console.log(newK, newC)
})

// 只监听对象的某个深层字段(不用 deep,更精准)
watch(() => user.value.age, (newAge) => {
  console.log('年龄变为', newAge)
})

computed VS watch

computedwatch 能不能互相替代?不能直接替代。

computed派生数据(返回新值给模板用,有缓存);watch响应变化做副作用(不产出新值)。

经验法则:模板的计算一律用 computed,只有在”变化后做事情”时才用 watch

watchEffect

watch 要手动告诉它”监听谁”;watchEffect什么都不用指定——它在回调执行时自动收集用到了哪些响应式数据,其中任何一个变了,就重新执行一次。

基本语法:watchEffect(回调)

适合”同步副作用”、又不想一个个列依赖的场景:

import { watchEffect } from 'vue'

const keyword = ref('')
const page = ref(1)

// 不需要声明依赖,函数里用了谁它就听谁
watchEffect(() => {
  document.title = `搜索:${keyword.value} - 第${page.value}页`
})

watch vs watchEffect

对比维度 watch watchEffect
依赖 手动指定监听目标 自动收集用到谁就听谁
拿到变化 newVal / oldVal 没有新旧值对比
初始执行 默认不执行(加 immediate 立即执行一次
场景 精确监听某个变化、需要旧值做对比 同步副作用,不用管依赖,如 title
异步操作 可以配合 await 发请求等 副作用里别做异步重活,保持轻量

一句话选择:

  • 需要”这个变了→做那个”,用 watch
  • 如果只是”凑齐运行一遍,谁变了我重跑”,用 watchEffect

速查卡片

场景 写法
派生数据(有缓存) const t = computed(() => ...)
读写都支持的派生值 computed({ get, set })
监听变化做副作用 watch(source, cb, { immediate, deep })
监听多个 / 某个字段 watch([a, b], cb) / watch(() => obj.prop, cb)
自动收集依赖的同步副作用 watchEffect(() => { ... })
上一篇
Vue3 响应式核心
下一篇
Vue3 条件渲染