Vue 3 监听属性完全指南
本文档从零开始讲解 Vue 3 的监听属性:如何在一段数据变化时自动执行一段逻辑(如请求接口、本地存储、校验表单),包括 watch、watchEffect 的用法、区别、常见场景和易错点,配有大量示例,适合新手系统学习。
一、什么是“监听”?
1.1 为什么需要监听?
页面上经常有这种需求:当某个数据变了,要做一件事。例如:
- 用户改了“用户名”,要去检查是否已被占用(请求接口)
- 路由参数
id变了,要重新拉取该 id 的详情 - 购物车数量变了,要把最新数据存到本地
- 某个输入框变了,要做格式校验或防抖请求
这类逻辑不是“算出一个新值”(那用计算属性),而是“发生某件事之后执行一段代码”(副作用)。在 Vue 里,用 监听 来实现:你声明“监听谁、变的时候做什么”,Vue 会在数据变化时自动执行你写的回调。
1.2 监听长什么样?
Vue 3 提供两种方式:
- watch:你明确指定“监听哪个数据”,回调里可以拿到新值和旧值,还可配置“是否深度监听、是否立即执行一次”等。
- watchEffect:你写一段会用到响应式数据的代码,Vue 自动收集这段代码里用到的依赖,只要其中任意一个变了,就重新执行这段代码。
下面先讲 watch,再讲 watchEffect,最后对比“什么时候用谁”。
二、watch:监听一个或多个数据源
2.1 基本语法
语法: watch(数据源, 回调函数, 可选配置)
- 数据源:可以是一个 ref、一个 getter 函数
() => xxx、或一个数组(多个数据源)。 - 回调函数:
(新值, 旧值) => { ... },在数据变化时执行。 - 可选配置:如
{ deep: true, immediate: true }等。
返回值:一个停止监听的函数,调用后就不再监听。
2.2 监听一个 ref
<template>
<div>
<input v-model="keyword" placeholder="输入关键词" />
<p>你输入了:{{ keyword }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
watch(keyword, (newVal, oldVal) => {
console.log('从', oldVal, '变成了', newVal)
})
</script>
- keyword 变化时(例如输入或删除字符),回调会执行。
- newVal:当前最新值;oldVal:上一次的值(首次执行时可能是 undefined,见下文 immediate)。
注意: 在 <script setup> 里,若数据源是 ref,直接传 keyword 即可(不要传 keyword.value),watch 会监听这个 ref 本身。
2.3 监听 getter(监听对象里的某个属性)
若想监听 对象里某一个属性,数据源要写成 getter 函数,返回你要监听的属性值:
<script setup>
import { ref, watch } from 'vue'
const user = ref({
name: '小明',
age: 18
})
// 只监听 user.name 的变化
watch(
() => user.value.name,
(newVal, oldVal) => {
console.log('name 从', oldVal, '变成', newVal)
}
)
</script>
这样只有 user.name 变化时才会触发,改 user.age 不会触发。
2.4 监听多个数据源(数组)
第一个参数传数组,回调的新值、旧值也是数组,顺序与数据源一致:
<script setup>
import { ref, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
watch(
[firstName, lastName],
([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('姓从', oldFirst, '变成', newFirst)
console.log('名从', oldLast, '变成', newLast)
}
)
</script>
任一一个 ref 变化,回调都会执行;回调里通过解构拿到每个的新值和旧值。
三、watch 的常用配置项
3.1 deep:深度监听(对象、数组内部变化)
默认情况下,watch 只监听数据源本身的替换(例如整个 ref 被赋成新对象)。
若数据源是对象或数组,你改的是里面的属性或元素(如 obj.a = 1、arr.push(1)),默认不会触发。
加上 deep: true 后,对象/数组内部的变化也会触发回调:
<script setup>
import { ref, watch } from 'vue'
const form = ref({
name: '',
age: 0
})
watch(
form,
(newVal, oldVal) => {
console.log('form 有变化', newVal)
},
{ deep: true }
)
</script>
此时改 form.value.name 或 form.value.age 都会触发。
注意: 使用 deep 时,newVal 和 oldVal 会指向同一个对象(因为 Vue 监听的是同一引用内部的变化),若需要“旧值”做对比,可以在回调里自己拷贝一份或用其它方式记录。
3.2 immediate:立即执行一次
默认是数据第一次变化时才执行回调;若希望一上来就执行一次(例如根据初始值请求接口),可加 immediate: true:
<script setup>
import { ref, watch } from 'vue'
const id = ref(1)
watch(
id,
(newVal) => {
console.log('当前 id:', newVal)
// 这里可以根据 id 请求详情接口
},
{ immediate: true }
)
</script>
加了 immediate 后,组件创建时就会执行一次;此时 oldVal 是 undefined(因为还没有“上一次”)。
3.3 同时用 deep 和 immediate
<script setup>
import { ref, watch } from 'vue'
const options = ref({ page: 1, size: 10 })
watch(
options,
(newVal) => {
console.log('options 变化,重新请求', newVal)
},
{ deep: true, immediate: true }
)
</script>
适合“一进来就根据 options 请求一次,之后 options 内部任何属性变了也再请求”的场景。
四、watch 的典型用法小结
| 数据源写法 | 含义 |
|---|---|
watch(ref变量, 回调) |
监听整个 ref(值或引用变化都会触发) |
watch(() => obj.value.xxx, 回调) |
只监听对象某个属性 |
watch([a, b], 回调) |
监听多个源,回调参数为 (新值数组, 旧值数组) |
{ deep: true } |
监听对象/数组内部变化 |
{ immediate: true } |
创建时先执行一次回调 |
五、停止监听
watch 会返回一个函数,调用这个函数就停止监听:
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
const stop = watch(count, (newVal) => {
console.log(newVal)
if (newVal >= 10) {
stop() // 到 10 就不再监听了
}
})
</script>
在组件被卸载时,若希望停止监听,可以在 onUnmounted 里调用这个停止函数(Vue 3 里在 setup 中创建的 watch 会在组件卸载时自动停止,手动 stop 常用于“条件满足就不再监听”等场景)。
六、watchEffect:自动收集依赖
watchEffect 不要求你显式写出“监听谁”,而是执行一个函数,函数里用到的 ref/reactive 会被自动收集为依赖;只要其中任意一个变了,函数会重新执行。
6.1 基本语法
语法: watchEffect(回调函数)
- 回调函数:无参数;里面可以随意使用多个响应式数据。
- 返回值:同样是停止监听的函数。
<template>
<div>
<input v-model="keyword" />
<p>关键词:{{ keyword }}</p>
</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue'
const keyword = ref('')
watchEffect(() => {
console.log('keyword 当前是:', keyword.value)
})
</script>
- 第一次会执行一次,打印当前 keyword。
- 之后只要 keyword 变化,就会再执行。
- 若你在回调里还用了别的 ref(如
count.value),count 也会自动成为依赖,变了也会触发。
6.2 和 watch 的对比(何时用谁)
| 对比项 | watch | watchEffect |
|---|---|---|
| 是否要写数据源 | 要,明确指定 ref/getter/数组 | 不用,自动根据回调里用到的依赖 |
| 能否拿到旧值 | 能,(newVal, oldVal) | 不能,只关心“当前值” |
| 是否默认执行 | 默认不执行(可配 immediate) | 默认会先执行一次 |
| 典型用途 | 需要旧值、需要精确控制监听谁 | 依赖多、逻辑简单、不需要旧值 |
简单记: 需要旧值或只监听某几个精确目标用 watch;依赖多、逻辑简单、一进来就要跑一次用 watchEffect。
七、常见使用场景示例
7.1 数据变化时请求接口(如搜索、详情)
<template>
<div>
<input v-model="keyword" placeholder="搜索" />
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const list = ref([])
watch(keyword, async (newVal) => {
if (!newVal.trim()) {
list.value = []
return
}
const res = await fetch('/api/search?q=' + encodeURIComponent(newVal))
const data = await res.json()
list.value = data.list
}, { immediate: false })
</script>
若希望“一进来就按空关键词请求一次”,可加 immediate: true。
7.2 路由参数变化时重新拉取详情
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const detail = ref(null)
watch(
() => route.params.id,
async (newId) => {
if (!newId) return
const res = await fetch(`/api/item/${newId}`)
detail.value = await res.json()
},
{ immediate: true }
)
</script>
用 getter 监听 route.params.id,并 immediate: true,进入页或 id 变化都会请求。
7.3 同步到本地存储(localStorage)
<script setup>
import { ref, watch } from 'vue'
const settings = ref({
theme: 'light',
fontSize: 14
})
watch(
settings,
(newVal) => {
localStorage.setItem('settings', JSON.stringify(newVal))
},
{ deep: true, immediate: true }
)
</script>
deep: true 保证改 settings 里任意字段都会存;immediate: true 保证一进来就把当前 settings 写进 localStorage(若之前已存过,可先在 onMounted 里从 localStorage 读出来赋给 settings)。
7.4 表单校验(某字段变化时校验)
<template>
<div>
<input v-model="form.username" placeholder="用户名" />
<p v-if="error">{{ error }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const form = ref({ username: '' })
const error = ref('')
watch(
() => form.value.username,
(newVal) => {
if (!newVal) {
error.value = '请输入用户名'
} else if (newVal.length < 3) {
error.value = '至少 3 个字符'
} else {
error.value = ''
}
}
)
</script>
7.5 防抖(输入停止一段时间后再请求)
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
let timer = null
watch(keyword, (newVal) => {
clearTimeout(timer)
timer = setTimeout(() => {
console.log('防抖后请求:', newVal)
// 这里写请求逻辑
}, 300)
})
</script>
在 onUnmounted 里最好 clearTimeout(timer),避免组件销毁后仍执行。
7.6 用 watchEffect 做“一进来就执行且依赖多个数据”
<script setup>
import { ref, watchEffect } from 'vue'
const page = ref(1)
const size = ref(10)
const list = ref([])
watchEffect(async () => {
const res = await fetch(`/api/list?page=${page.value}&size=${size.value}`)
list.value = await res.json()
})
</script>
page 或 size 任一变化都会重新请求;并且一进来就会执行一次。
八、易错点与注意点
8.1 避免在监听回调里改被监听的数据(死循环)
若在 watch 或 watchEffect 的回调里又修改了被监听的数据,会再次触发监听,可能造成无限循环。
例如:
watch(count, () => {
count.value++ // 危险:count 变了又触发 watch,又 ++ ...
})
需要“数据 A 变时改数据 B、数据 B 变时改数据 A”时,要加条件或标志位,避免互相触发。
8.2 监听“对象整体”时,newVal 和 oldVal 可能相同
使用 deep: true 监听对象时,回调里的 newVal 和 oldVal 会指向同一个对象(当前引用),因为 Vue 监听的是引用内部变化。若需要“旧值”做对比,可在回调内用 拷贝 或自己维护一份旧值。
8.3 watchEffect 会立即执行一次
若你有一段“只想在依赖变化时跑、首次不跑”的逻辑,用 watch 且不要设 immediate;若用 watchEffect,可在回调里加条件判断,或改用 watch。
九、监听属性速查表
| 需求 | 写法 |
|---|---|
| 监听一个 ref | watch(ref名, (newVal, oldVal) => { }) |
| 监听对象某属性 | watch(() => obj.value.xxx, 回调) |
| 监听多个 | watch([a, b], ([newA, newB], [oldA, oldB]) => { }) |
| 深度监听 | 第三个参数 { deep: true } |
| 立即执行一次 | 第三个参数 { immediate: true } |
| 自动收集依赖、立即执行 | watchEffect(() => { ... }) |
| 停止监听 | 使用 watch/watchEffect 的返回值:const stop = watch(...); stop() |
十、学习建议
- 先掌握 watch(数据源, 回调),能监听一个 ref、一个 getter、以及 deep / immediate 的用法。
- 理解 watch 和 watchEffect 的区别:要不要旧值、要不要精确指定数据源、是否默认执行一次。
- 常见用法:数据变→请求接口、路由参数变→拉详情、表单变→校验或存 localStorage、防抖请求。
- 避免在回调里修改被监听的数据导致死循环;需要“互相联动”时加条件或拆分逻辑。
把本文档里的示例在项目里敲一遍、改数据看回调是否触发,会掌握得更牢。祝你学习顺利。