Appearance
侦听器
计算属性侧重于监听响应式数据的变化,并派生出计算后的新值。
而侦听器侧重于监听响应式数据的变化,并在状态变化之后执行一些“副作用”:例如更改 DOM,执行 Ajax 操作等。例如:实时侦听用户输入的账号名,并调用 Ajax 接口检测账号是否已被注册。
基本语法
侦听器的基本语法格式如下:
js
import { watch } from 'vue'
watch(响应式数据, fn)
watch(响应式数据, (新值,旧值) => { /* 副作用操作 */ })
例如:实时监听 title
值的变化,动态设置网页的标题:
js
setup() {
// 源数据
const title = ref('')
// 监听 title 值的变化,实时设置网页的标题
// 参数1:新值; 参数2:旧值
watch(title, (newTitle, oldTitle) => {
document.title = newTitle
})
return {
title
}
}
页面的模板结构如下:
html
<div id="app">
<input type="text" v-model="title" />
</div>
侦听对象上简单属性的变化
如果响应式对象中的属性值为简单类型(数字、字符串、布尔值),则需要使用 getter 函数指定要侦听的数据项:
js
const user = ref({
name: 'zs',
address: {
province: '河北省',
city: '邯郸市'
}
})
// ❌错误写法
watch(user.value.name, (newName, oldName) => {
console.log(newName, oldName)
})
// ✅正确写法
watch(
() => user.value.name,
(newName, oldName) => {
console.log(newName, oldName)
}
)
// ✅正确写法
watch(
() => user.value.address.city,
(newCity, oldCity) => {
console.log(newCity, oldCity)
}
)
侦听对象上复杂属性的变化
例如,在刚才的 user
对象中,如果想要侦听 user.address
的变化,不同的侦听方式会产生不同的侦听效果,请各位同学注意辨别:
js
const user = ref({
name: 'zs',
address: {
province: '河北省',
city: '邯郸市'
}
})
watch(obj, fn)
TIP
- 能够侦听到 obj 对象上属性的变化,例如:修改 address.city 的值会被侦听到;
- 无法侦听到 user.value.address 对象的替换操作。
js
// 仅在嵌套的属性变更时触发
watch(user.value.address, (newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
console.log(newValue, oldValue)
})
const updateAddress = () => {
user.value.address.city = '石家庄' // ✅可以正常侦听到对象中嵌套属性的变化
user.value.address = { a: 1, b: 2 } // ❌无法侦听到对象的替换操作
}
html
<button @click="updateAddress">修改 address</button>
watch(() => obj, fn)
TIP
- 能够侦听到 user.value.address 对象的替换操作;
- 无法侦听到 obj 对象上属性的变化,例如:修改 address.city 的值不会被侦听到;
js
// 仅当 user.value.address 对象被替换时触发
watch(
() => user.value.address,
(newValue, oldValue) => {
console.log(newValue, oldValue)
}
)
const updateAddress = () => {
user.value.address.city = '石家庄' // ❌无法侦听到对象中嵌套属性的变化
user.value.address = { a: 1, b: 2 } // ✅可以侦听到对象的替换操作
}
html
<button @click="updateAddress">修改 address</button>
deep 深层侦听器
TIP
getter 配合 deep 选项一起使用时,既能够侦听到对象的替换操作,又能够侦听到对象属性的变化。
js
watch(
() => user.value.address,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* user.value.address 被整个替换了
console.log(newValue, oldValue)
},
{ deep: true }
)
const updateAddress = () => {
user.value.address.city = '石家庄' // ✅可以侦听到对象中嵌套属性的变化
user.value.address = { a: 1, b: 2 } // ✅可以侦听到对象的替换操作
}
html
<button @click="updateAddress">修改 address</button>
TIP
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
侦听器的高级用法
即时回调的侦听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。
我们可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
js
// 源数据
const title = ref('哇哈哈')
// 侦听器
watch(
title,
(newTitle, oldTitle) => {
document.title = newTitle
},
{
immediate: true
}
)
一次性侦听器
每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true
选项。
js
// 监听 title 值的变化,实时设置网页的标题
watch(
title,
(newTitle, oldTitle) => {
document.title = newTitle
},
{
immediate: true,
once: true
}
)
侦听多个数据的变化
watch 还可以通过数组的形式,侦听多个数据源的变化,只要被侦听的任何一个数据源发生了变化,就会触发 watch 回调函数的执行。语法格式如下:
js
// 通过数组的形式,同时侦听多个数据源的变化
watch([value1, value2], ([newValue1, newValue2], [oldValue1, oldValue2]) => {
// watch 的回调函数
})
例如:
js
setup() {
// 源数据
const title = ref('哇哈哈')
const suffix = ref('dev')
// 同时侦听 title 和 suffix 的变化
watch([title, suffix], ([newTitle, newSuffix]) => {
document.title = `${newTitle} - ${newSuffix}`
}, {
immediate: true
})
return {
title,
suffix
}
}
页面的模板结构如下:
html
<input type="text" v-model="title" />
<br />
<input type="text" v-model="suffix" />
watchEffect()
使用 watch 侦听器时需要程序员手动指定依赖项,再通过回调函数的形参接收变化前后的新旧数据,最后才能在回调函数中使用变化后的数据,用法比较繁琐:
js
watch(依赖项, (新数据, 旧数据) => {
// 使用形参中接收到的新/旧数据
})
为了简化 watch 侦听器的用法,vue 提供了 watchEffect
API,它不需要手动指定依赖项,而是在运行时自动收集依赖:
js
// watchEffect(fn)
// 在页面首次渲染时,watchEffect 中的回调函数会立即运行一次,
// 从而收集“响应式数据”和“副作用函数”的依赖关系
watchEffect(() => {
document.title = `${title.value} - ${suffix.value}`
})
watch
vs watchEffect
相同点
都能侦听响应式数据的变化,并执行副作用函数。
不同点
- watch 需要显示指定依赖项,而 watchEffect 不需要显示指定依赖项。watchEffect 会在首次执行时自动收集依赖关系。
- watch 的回调函数在首次渲染时默认不执行(可配置
immediate: true
选项改为立即执行),而 watchEffect 会在首次渲染时执行。
使用场景
- 如果想提高代码的可读性、精确控制回调函数的触发时机:推荐使用 watch 侦听器,因为 watch 必须明确指定依赖项,且只在依赖项变化时才会触发回调。
- 如果想追求代码的简洁性,对响应性的依赖关系要求不高:推荐使用 watchEffect,因为它会在首次执行回调函数时自动收集依赖关系,能极大提高代码的简洁性。
回调的触发时机
默认情况下,watch 和 watchEffect 的回调函数会在数据变更后、DOM 更新前执行。因此在回调函数中获取到的是 DOM 更新前的旧状态,而非更新后的新状态。例如:
js
setup() {
const count = ref(0) // 初始值 0
// 侦听 count 值的变化,输出 h1 的文本内容
watch(count, (newValue) => {
const h1DOM = document.querySelector('h1')
// 输出 count 的值是:0,而不是更新后的 1
console.log(h1DOM.textContent)
})
return {
count
}
}
html
<div id="app">
<h1>count 的值是:{{ count }}</h1>
<button @click="count++">+1</button>
</div>
TIP
默认情况下,回调函数会在 DOM 更新之前调用。
Post Watchers
如果想在侦听器的回调函数中获取到更新后的 DOM,则需要把回调函数延迟到 DOM 更新完毕之后执行。此时可以使用 flush: 'post'
选项。语法格式如下:
js
watch(source, callback, {
flush: 'post'
})
watchEffect(callback, {
flush: 'post'
})
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
js
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 DOM 更新后执行 */
})
代码示例
watch + post 的用法示例:
js
// 侦听 count 值的变化,输出 h1 的文本内容
watch(
count,
(newValue) => {
const h1DOM = document.querySelector('h1')
console.log(h1DOM.textContent)
},
{
flush: 'post'
}
)
watchEffect + post 的用法示例:
js
watchEffect(
() => {
console.log(count.value)
const h1DOM = document.querySelector('h1')
console.log(h1DOM?.textContent)
},
{
flush: 'post'
}
)
watchPostEffect 的用法示例:
js
watchPostEffect(() => {
console.log(count.value)
const h1DOM = document.querySelector('h1')
console.log(h1DOM?.textContent)
})
同步侦听器*
TIP
同步侦听器可能存在性能上的问题,因此在实际开发中请谨慎使用!
默认情况下,侦听器的“回调函数”会被批量处理以避免重复调用(这种批处理的处理方式是高效的、性能友好的)。如果我们同步的连续修改被侦听的数据多次,出于性能考虑,我们不希望连续触发多次侦听器的回调函数:
html
<div id="app">
<h1>count 的值是:{{ count }}</h1>
<button @click="addThreeTimes">循环3次+1</button>
</div>
js
setup() {
const count = ref(0) // 初始值 0
// 点击按钮,循环让 count 自增 +1 三次
const addThreeTimes = () => {
for (let i = 0; i < 3; i++) {
count.value++
}
}
// 侦听 count 值的变化
watch(count, (newVal) => {
// 点击1次按钮,只输出了“监听到了 count 值的改变:3”
// 更新期间的中间状态(1和2)被忽略了,能够提高性能
console.log('监听到了 count 值的改变:' + newVal)
})
return {
count,
addThreeTimes
}
}
如果您想侦听到中间状态的变化,并在每次状态变化后都触发回调函数,vue 提供了 flush: 'sync'
选项来创建同步触发的侦听器。语法格式如下:
js
watch(source, callback, {
flush: 'sync'
})
watchEffect(callback, {
flush: 'sync'
})
同步触发的 watchEffect()
有个更方便的别名 watchSyncEffect()
:
js
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})
示例代码如下:
js
watch(
count,
(newVal) => {
// 点击1次按钮,输出了三次,中间状态的变化也会触发侦听器的回调函数:
// 监听到了 count 值的改变:1
// 监听到了 count 值的改变:2
// 监听到了 count 值的改变:3
console.log('监听到了 count 值的改变:' + newVal)
},
{
flush: 'sync'
}
)
// --------
watchEffect(
() => {
console.log('监听到了 count 值的改变:' + count.value)
},
{
flush: 'sync'
}
)
// --------
watchSyncEffect(() => {
console.log('监听到了 count 值的改变:' + count.value)
})
停止侦听器
要手动停止一个侦听器,请调用 watch
或 watchEffect
返回的函数:
js
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch()
示例的代码如下:
html
<div id="app">
<input type="text" v-model="title" />
<hr />
<button @click="stop">停止侦听器</button>
<button @click="start">开启侦听器</button>
</div>
js
setup() {
// 响应式数据
const title = ref('论')
// 变量,用来存储侦听器返回的 unwatch 函数
let unwatch = null
// 点击按钮停止侦听器
const stop = () => {
unwatch && unwatch()
unwatch = null
console.log('停止了')
}
// 点击按钮开启侦听器
const start = () => {
if (unwatch) return // 防止开启多余的侦听器
// watch
unwatch = watch(title, (newVal) => {
document.title = newVal
}, {
immediate: true
})
// watchEffect
// unwatch = watchEffect(() => document.title = title.value)
}
// 首次渲染时,自动启动侦听器
start()
return {
title,
stop,
start
}
}