目录
Vue 3 watch
基本用法
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)
// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>
副作用刷新时机
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick”中多个状态改变导致的不必要的重复调用。
同一个“tick”的意思是,Vue的内部机制会以最科学的计算规则将视图刷新请求合并成一个一个的"tick",每个“tick”刷新一次视图,比如a=1;b=2;
只会触发一次视图刷新。$nextTick的Tick就是指这个。
flush
选项
Vue 3 提供了 flush
选项来控制 watch
回调的执行时机。flush
有三个可选值:pre
、post
和 sync
。
flush: 'pre'
- 触发时机:在响应式依赖项的值更新之前执行回调。
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用。例如,如果我们同步将一千个项目推入被侦听的数组中,我们可能不希望侦听器触发一千次。
默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
<template>
<div>
<button
id="aa"
@click="
() => {
r++
s++
}
"
>
{{ r }} - {{ s }}
</button>
</div>
</template>
<script>
import { ref, watch } from 'vue'
export default {
setup() {
let r = ref(2)
let s = ref(10)
watch([() => s.value, () => r.value], () => {
console.log(r.value, s.value)
console.log(
document.querySelector('#aa') && document.querySelector('#aa').innerText
)
})
return {
r,
s
}
}
}
</script>
点击若干次(比如2次)按钮,得到的结果是:
当我第1和2次点击完,你会发现,document.querySelector('#aa').innerText
获取到的总是点击之前DOM的内容。这也说明,默认Vue先执行监听器,所以取到了上一次的内容,然后执行组件update。
flush: 'post'
- 触发时机:在响应式依赖项的值更新之后执行回调。
如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明
flush: 'post'
选项:
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
没设flush: 'post' | 设了flush: 'post' |
---|---|
所以结论是,如果要操作“更新之后的DOM”,就要配置flush: 'post'。
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
/* 在 Vue 更新后执行 */
})
flush: 'sync'
- 触发时机:在数据变化的同步代码块中执行回调。
你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:
同步触发的 watchEffect()
有个更方便的别名 watchSyncEffect()
:
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
/* 在响应式数据变化时同步执行 */
})
- 如果flush设置为“sync”,一旦值发生变化,回调将被同步调用。
- 对于“pre”和“post”,回调都使用队列进行缓冲。即使监视值多次更改,回调也只会添加到队列中一次。临时值将被跳过并且不会传递给回调。
- 缓冲回调不仅可以提高性能,还有助于确保数据一致性。在执行数据更新的代码完成之前,不会触发观察者
- 应谨慎使用“同步”观察者,因为它们没有这些好处。
- 适用于需要立即响应数据变化的情况,例如实时计算或紧急数据更新。
谨慎使用
同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。
<script>
import { watch, ref } from 'vue'
export default {
setup() {
const count = ref(0)
watch(
count,
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal} (pre)`)
},
{ flush: 'pre' }
)
watch(
count,
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal} (post)`)
},
{ flush: 'post' }
)
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal} (pre)`)
})
watch(
count,
(newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal} (sync)`)
},
{ flush: 'sync' }
)
function increment() {
count.value++
}
return { count, increment }
}
}
</script>
<template>
<div @click="increment">{{ count }}</div>
</template>
<style scoped></style>