Vue3里怎么用watch监听input框输入?有哪些细节要注意?
平时做项目,input框的监听肯定是高频需求,比如实时校验用户输入的手机号格式、做搜索建议的触发、同步表单输入到vuex/pinia状态里这些,Vue2的时候用watch还算顺手,但到了Vue3,因为组合式API和响应式系统的变化,好多新手要么不知道怎么绑定、要么监听不到、要么频繁触发性能不行,今天就把这个问题拆透,从基础写法到进阶优化,再到常见坑点,慢慢说清楚。
基础的input监听怎么写?先分v-model绑定的数据类型说
Vue3监听input最常用的肯定是组合式API里的watch函数,不过得先搞清楚你用v-model绑定的是普通响应式变量还是ref/reactive的对象属性,写法不一样。
情况1:绑定的是ref声明的普通字符串/数字
这个最简单,新手入门大概率先遇到这种,比如做个简单的输入框,输入内容实时显示在旁边的label里。
先回忆下,Vue3里声明响应式基础类型用ref,然后给input加v-model="变量名",再在setup或者<script setup>里写watch就行。
举个具体的例子,在<script setup>语法糖里(这个现在是主流,比setup函数返回要方便太多):
<template>
<div class="simple-input">
<input type="text" v-model="userName" placeholder="请输入用户名" />
<p>你输入的是:{{ userName }}</p>
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
const userName = ref('')
// 基础监听写法:第一个参数是要监听的源,第二个是回调函数
watch(userName, (newVal, oldVal) => {
console.log('旧值:', oldVal)
console.log('新值:', newVal)
})
</script>
这种写法下,只要你在input里敲一个字,watch的回调就会执行一次,打印新旧值对比。
情况2:绑定的是reactive声明的对象属性
如果用reactive声明了一个表单对象,比如const form = reactive({ userName: '', password: '' }),这时候直接写watch(form.userName, ...)行不行?
可以,但要注意,如果要监听多个reactive的属性,或者监听整个对象(虽然不推荐,性能不好),写法得调整。
先看监听单个reactive属性的标准写法,其实直接传属性值或者传一个返回属性值的函数都可以,但传函数更稳妥,尤其是后面要做深度监听或者监听多个源的时候:
<template>
<div class="form-input">
<input type="text" v-model="form.userName" placeholder="请输入用户名" />
<input type="password" v-model="form.password" placeholder="请输入密码" />
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
const form = reactive({ userName: '', password: '' })
// 写法1:直接传reactive属性值
watch(form.userName, (newVal) => {
console.log('用户名变了:', newVal)
})
// 写法2:传箭头函数返回属性值(推荐,更通用)
watch(() => form.password, (newVal) => {
console.log('密码变了:', newVal)
})
</script>
为什么说传函数更通用?比如后面要监听多个源,比如同时监听用户名和密码,直接传数组就行,但单个源用数组里的箭头函数更统一:
// 同时监听多个reactive属性
watch([() => form.userName, () => form.password], ([newName, newPwd], [oldName, oldPwd]) => {
console.log('表单任意字段更新')
})
想让监听更灵活?试试watch的第三个配置参数
基础的写法只能满足“值变了就触发”的需求,但实际开发里,还有“组件刚挂载就触发一次”“监听对象内部的深层变化”“限制触发频率,别一敲字就触发”这些要求,这时候就要靠watch的第三个参数——配置对象了。
配置1:immediate:组件挂载后立即执行回调
这个参数太常用了!比如做搜索建议的页面,刚进页面的时候后端可能会返回热门搜索关键词,这时候如果用户没输入就自动显示热门词,或者刚进入页面要校验一下表单的初始值(虽然初始值一般是空的,但有时候有默认值比如编辑场景),就需要immediate: true。
比如拿刚才的用户名输入举编辑场景的例子,假设我们从后端拿了个初始用户名:
const userName = ref('张三')
watch(userName, (newVal, oldVal) => {
console.log('旧值:', oldVal)
console.log('新值:', newVal)
}, {
immediate: true // 组件刚挂载,userName还是初始值'张三',就会触发一次回调,旧值是undefined
})
这里要注意,immediate为true的时候,第一次触发的回调里,oldVal是undefined,因为还没有历史更新记录。
配置2:deep:深度监听对象/数组的内部变化
刚才说reactive直接传整个对象watch(form, ...),如果不加deep: true,是监听不到对象内部属性变化的,比如form.userName变了,整个form的引用地址没变,默认的浅监听就不会触发,那什么时候需要深度监听?
比如监听一个嵌套很深的对象,比如用户的收货地址form = reactive({ address: { province: '北京', city: '朝阳区', street: '望京SOHO' } }),如果要监听省份、城市、街道的任意变化,就可以用深度监听整个form.address:
const form = reactive({
address: {
province: '北京',
city: '朝阳区',
street: '望京SOHO'
}
})
// 深度监听整个address对象
watch(() => form.address, (newAddress, oldAddress) => {
console.log('收货地址变了:', newAddress)
}, {
deep: true
})
深度监听的性能损耗比较大,因为Vue3要递归遍历整个对象/数组,监听每一个内部的响应式属性,所以能不用就不用,尽量只监听你需要的具体属性,或者后面讲的watchEffect有时候能替代但也不是万能的。
配置3:flush:控制回调的执行时机
这个参数新手可能用得少,但做DOM操作或者第三方库集成的时候会遇到,默认值是'post',意思是在Vue的DOM更新之后执行回调;如果设为'pre',就是在DOM更新之前执行;还有'sync',同步执行,一般不推荐,容易卡死。
比如举个DOM操作的例子,假设输入框输入内容之后,要实时获取输入框的滚动位置或者高度(比如textarea自动高度),这时候就需要用默认的'post'或者明确设置,因为'pre'的时候DOM还没更新,获取到的是旧的高度:
<template>
<div class="auto-height-textarea">
<textarea
ref="textareaRef"
v-model="content"
placeholder="请输入内容,会自动调整高度"
></textarea>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
const content = ref('')
const textareaRef = ref(null)
// 默认flush: 'post',DOM更新后执行
watch(content, () => {
if (textareaRef.value) {
// 或者更稳妥的话,不管默认flush是什么,都包一层nextTick,双重保险
nextTick(() => {
textareaRef.value.style.height = 'auto'
textareaRef.value.style.height = textareaRef.value.scrollHeight + 'px'
})
}
})
</script>
这里虽然默认是'post',但加nextTick也没问题,因为nextTick也是等待DOM更新队列清空后执行,双重保险不会错。
一敲字就触发监听?太频繁了!怎么优化性能?
这是搜索建议、表单远程校验这类场景的常见痛点,比如你做一个搜索框,一敲字就调后端接口,不仅浪费流量,后端接口还可能因为请求太快返回乱序(比如先敲的“手”后敲的“手机”,但“手机”的接口先回来,“手”的接口后回来,覆盖了正确的结果),这时候就需要防抖(debounce)或者节流(throttle)了。
先说说防抖:用户停止输入一段时间后,才触发回调
防抖最适合搜索建议、远程校验这种场景,核心逻辑是“等一等再做,等的过程中如果有新输入,就重新计时”。 Vue3本身没有内置防抖节流函数,不过我们可以自己手写一个简单的,或者用lodash的debounce/throttle(现在项目里基本都会装lodash-es,按需引入就行,别装整个lodash包,太占体积)。 先看手写防抖的版本(更轻量,适合不想装lodash的项目):
// 手写一个简单的防抖函数
const debounce = (fn, delay = 500) => {
let timer = null
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
// 然后在watch的回调里用
watch(userName, debounce((newVal) => {
if (newVal.trim()) {
// 调用搜索接口
console.log('调用搜索接口,关键词:', newVal)
}
}, 600)) // 这里的600是延迟时间,一般500-800ms比较合适,根据实际需求调
再看用lodash-es的版本(更稳定,支持更多配置,比如leading=true就是第一次输入立即触发,然后等一段时间再触发):
// 先安装lodash-es:npm install lodash-es --save
// 然后按需引入
import { debounce } from 'lodash-es'
watch(userName, debounce((newVal) => {
if (newVal.trim()) {
console.log('调用搜索接口,关键词:', newVal)
}
}, 600, { leading: false, trailing: true })) // leading=false是默认,等最后一次输入;trailing=true也是默认
这里要注意,watch的回调函数不能直接传箭头函数再包debounce吗?可以,但手写的或者lodash的debounce返回的是一个新函数,所以没问题,但如果是在setup函数里(不是
code前端网