watch是干啥的?怎么用?
咱做Vue开发时,肯定绕不开watch和computed这俩工具,尤其是刚接触Vue3组合式API的同学,经常懵:这俩都能响应数据变化,到底有啥不一样?啥场景用哪个?今天咱就掰开揉碎了聊,从“是干啥的”“咋用”到“区别”“场景选择”,一次讲透~
watch的核心作用是监听响应式数据变化,触发“副作用”逻辑。“副作用”指发请求、修改DOM、打印日志这类不直接返回值,却要做额外操作的事儿,下面从“监听的源”“回调逻辑”“配置选项”三部分拆解用法:
监听的“源”可以是啥?
Vue3里watch能监听的源很灵活:
- 单个ref/reactive变量:若监听
ref
定义的数值(如const count = ref(0)
),直接传watch(count, (newVal) => { ... })
;若监听reactive
对象的属性,得用“getter函数”,比如const state = reactive({ name: '张三' })
,要写成watch(() => state.name, (newName) => { ... })
——这样只监听name
变化,性能更优,不用对整个state
开深度监听。 - 多个源组成的数组:若需同时监听
count
和name
,传数组watch([count, () => state.name], ([newCount, newName]) => { ... })
,回调里新值也呈数组形式。 - 复杂getter函数:甚至能监听“派生后的数据”,比如
watch(() => state.list.length, (newLen) => { ... })
,专门盯数组长度变化。
回调函数里能干啥?
回调接收newVal
(变化后的值)和oldVal
(变化前的值)两个参数,但要注意:若监听reactive
对象,oldVal
和newVal
可能指向同一引用(因reactive
是代理对象,修改内部属性不会换引用),这时候得自己判断变化的字段。
举个实际例子:用户输入搜索关键词,每次变化后发请求获取列表,代码如下:
const searchKey = ref('') watch(searchKey, (newKey) => { // 发请求拿数据,这是典型“副作用” fetchList(newKey).then(res => { list.value = res.data }) })
配置选项里的门道
watch第三个参数是配置对象,常用选项有这些:
immediate: true
:组件一加载,立刻执行一次回调,比如页面加载时,需根据默认searchKey
先请求数据,就加这个。deep: true
:对reactive对象做“深度监听”,比如state
是嵌套对象{ user: { age: 18 } }
,若直接传state
当源,得开deep
才能监听到user.age
变化,但注意!deep
会遍历对象所有属性,数据复杂时性能拉胯,所以优先用getter函数监听具体属性(如() => state.user.age
),别依赖deep
。flush: 'pre' | 'post' | 'sync'
:控制回调执行时机,默认pre
(组件更新前执行),post
是组件更新后(适合操作DOM,因这时DOM已更新),sync
是数据一变立刻执行(少用,性能风险高)。
举个deep
的反面例子:若state
是多层嵌套的大对象,开deep
监听整个state
,每次哪怕改个小属性,Vue都得递归遍历所有子属性,性能肯定崩,所以尽量精准监听,用getter函数只盯需要的字段。
computed又是干啥的?用法有啥特点?
computed叫“计算属性”,核心是基于已有响应式数据,派生新的响应式数据,且自动缓存结果,说白点,把多个数据的计算逻辑封装成新变量,仅在依赖变化时重新计算”。
为啥要用computed?和methods有啥区别?
先对比methods:比如页面要显示“全名”,由firstName
和lastName
拼接,若用methods,写个fullName()
方法,每次组件渲染都会执行;但用computed,定义const fullName = computed(() => firstName.value + lastName.value)
,只有firstName
或lastName
变化时,才会重新计算,否则直接拿缓存结果。
所以computed的缓存机制是关键:避免重复计算,提升性能,尤其计算逻辑复杂(如遍历大数组、多次运算)时,缓存能省很多性能。
computed的两种写法
- 函数式(只读):最常用,比如上面的
fullName
,直接返回计算后的值:const fullName = computed(() => { return firstName.value + ' ' + lastName.value })
- 对象式(可写,带get/set):适合“双向绑定”场景,比如用户编辑个人信息,页面上
v-model
绑定到computed,这时需要setter
来更新源数据:const fullName = computed({ get() { return user.firstName + ' ' + user.lastName }, set(newVal) { // 拆分新值,更新源数据 const [first, last] = newVal.split(' ') user.firstName = first user.lastName = last } })
页面里用
v-model="fullName"
,输入新全名时,setter
会自动触发,更新user
的firstName
和lastName
。
computed的“惰性”和“缓存”
computed是惰性计算的:只有“访问”这个计算属性时,才会执行计算逻辑,而且只要依赖的响应式数据没变化,每次访问拿到的都是缓存结果。
举个例子:购物车计算总价,依赖商品列表goodsList
和每个商品的price
,只要goodsList
和price
没变化,不管页面渲染多少次,totalPrice
的计算逻辑只执行一次,之后直接拿缓存,代码:
const totalPrice = computed(() => { console.log('计算总价~') // 只在依赖变化时打印 return goodsList.value.reduce((sum, item) => sum + item.price * item.quantity, 0) })
若用methods写calcTotalPrice()
,每次渲染都得执行reduce
,数据多的时候就卡了。
watch和computed核心区别在哪?
从功能本质、依赖处理、执行时机、缓存机制几个维度对比,区别一目了然:
维度 | watch | computed |
---|---|---|
功能本质 | 监听变化,执行“副作用”(异步、复杂逻辑) | 派生新的响应式数据(同步、纯计算) |
依赖处理 | 可监听多个源(ref/reactive/getter) | 自动收集依赖(仅基于响应式数据) |
执行时机 | 数据变化后触发回调 | 依赖变化时惰性计算(访问时执行) |
缓存机制 | 无缓存(每次变化都执行回调) | 有缓存(依赖不变则复用结果) |
副作用允许 | 允许(发请求、改DOM等) | 不允许(必须纯函数,不能有副作用) |
举个直观例子:用户输入用户名,实时验证是否已存在。
- 用watch:监听
username
变化,发请求验证(异步副作用),把结果存到isValid
里。 - 用computed:若验证是同步逻辑(如正则匹配长度),可用computed返回是否合法;但若是异步请求(必须等接口返回),computed就不行——因computed必须同步返回值,这时候只能用watch。
啥时候用watch?啥时候用computed?
结合实际场景选工具,才是效率开发的关键:
用watch的场景:
- 需要处理异步逻辑:如数据变化后发请求、操作定时器,像搜索关键词变化后发请求(前面的例子),或用户登录状态变化后,异步加载权限菜单。
- 需要监听多个数据变化,组合逻辑:如购物车数量变化+商品单价变化,同时满足时才执行某逻辑,用watch监听这两个源,在回调里判断是否触发。
- 需要对变化做复杂处理:如数据变化后修改DOM(得用
flush: 'post'
确保DOM已更新)、记录操作日志、修改多个响应式数据的联动逻辑。 - 需要精细控制执行时机:如用
flush: 'post'
在DOM更新后操作DOM,避免拿到旧DOM状态;或用immediate
让组件加载时先执行一次逻辑。
举个场景:用户选择城市后,异步加载该城市的景点列表,同时记录选择日志,代码:
const city = ref('北京') watch(city, async (newCity) => { // 异步请求 const spots = await fetchSpots(newCity) spotList.value = spots // 记录日志(副作用) logUserAction(`选择城市:${newCity}`) }, { immediate: true }) // 页面加载时先请求北京的景点
用computed的场景:
- 需要派生一个新的响应式数据:如全名(
firstName+lastName
)、购物车总价、过滤后的列表(如搜索时,根据关键词过滤商品列表)。 - 需要缓存计算结果,避免重复计算:如遍历大数组做筛选、多次数学运算,用computed缓存结果,减少性能消耗。
- 需要定义“可写”的计算属性:如表单的双向绑定场景,用computed的
setter
同步更新多个源数据(前面的fullName
例子)。
举个过滤列表的例子:页面有搜索框,实时过滤商品列表,用computed缓存过滤结果,避免每次输入都重新遍历整个列表:
const searchKey = ref('') const goodsList = ref([...]) // 原始商品列表 const filteredList = computed(() => { return goodsList.value.filter(item => { return item.name.includes(searchKey.value) }) })
这样每次searchKey
变化时,filteredList
才会重新计算,否则直接用缓存,性能比每次调用methods里的过滤函数好太多。
容易踩的坑和最佳实践
最后补点避坑指南,让大家用得更顺:
watch的坑:
- 监听
reactive
对象时,别直接传对象+开deep
!性能爆炸,优先用getter函数监听具体属性,比如watch(() => state.user.age, ...)
,而非watch(state, ..., { deep: true })
。 - 回调里若修改响应式数据,注意循环触发,比如watch监听
count
,回调里又改count
,会无限循环,得加判断条件,如if (newVal !== oldVal)
再执行。
computed的坑:
- 不能有副作用!如在computed里发请求、修改其他响应式数据、操作DOM,都是错误的,因computed是“计算值”,设计上是纯函数,有副作用会导致逻辑混乱,还可能触发无限更新。
- 别把computed当methods用,若计算逻辑不需要缓存(如每次调用都要 fresh 结果),就用methods,比如点击按钮时执行的临时计算,用methods更合适。
watch和computed都是Vue3响应式系统的核心,但分工明确:watch负责“监听变化做动作”,适合异步、复杂逻辑;computed负责“派生新数据+缓存”,适合同步、纯计算,记住它们的区别和场景,写代码时就不会纠结选哪个啦~要是还有细节没搞懂,评论区随时聊~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。