vue3学习——侦听器
一、侦听器的基本概念
计算属性允许我们声明性地计算衍生值,但在一些场景下,比如状态变化时执行副作用(像更改 DOM、根据异步操作结果修改其他状态),就需要用到侦听器。简单来说,侦听器就像一个 “数据变化监控器”,当它 “观察” 的数据发生变化,就会执行我们预先设定的代码。
二、选项式 API 中的侦听器
在选项式 API 里,我们使用watch
选项来创建侦听器。示例代码如下:
export default {data() {return {question: '',answer: 'Questions usually contain a question mark. ;-)',loading: false}},watch: {// 每当question改变时,这个函数就会执行question(newQuestion, oldQuestion) {if (newQuestion.includes('?')) {this.getAnswer()}}},methods: {async getAnswer() {this.loading = truethis.answer = 'Thinking...'try {const res = await fetch('https://yesno.wtf/api')this.answer = (await res.json()).answer} catch (error) {this.answer = 'Error! Could not reach the API. ' + error} finally {this.loading = false}}}
}
在上述代码中,watch
选项监听question
数据的变化。一旦question
发生改变,就会执行对应的函数。在函数内部,检查新的question
是否包含?
,如果包含,则调用getAnswer
方法。getAnswer
方法用于发起异步请求获取答案,并处理请求过程中的加载状态和错误情况。
watch
选项还支持监听对象的嵌套属性,通过用.
分隔的路径来设置键,如:
export default {watch: {// 注意:只能是简单的路径,不支持表达式。'some.nested.key'(newValue) {//...}}
}
三、组合式 API 中的侦听器
在组合式 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 = trueanswer.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>
这里通过watch
函数监听question
这个ref
,当question
变化时,执行回调函数,逻辑和选项式 API 中的类似。
(一)侦听数据源类型
watch
函数的第一个参数可以是多种形式的 “数据源”:
- 单个 ref:可以直接侦听一个
ref
,如watch(x, (newX) => { console.log(
x is ${newX}) })
。 - getter 函数:通过返回一个值的函数来侦听,如
watch(() => x.value + y.value, (sum) => { console.log(
sum of x + y is: ${sum}) })
。 - 多个数据源组成的数组:同时侦听多个数据源,如
watch([x, () => y.value], ([newX, newY]) => { console.log(
x is ${newX} and y is ${newY}) })
。
注意,不能直接侦听响应式对象的属性值,比如
const obj = reactive({ count: 0 }); watch(obj.count, (count) => { console.log(
Count is: ${count}) })
这样是错误的,需要使用返回该属性的getter函数,即watch(() => obj.count, (count) => { console.log(
Count is: ${count}) })
。
(二)深层侦听器
默认情况下,watch
是浅层侦听,即被侦听的属性只有在被赋新值时才会触发回调,嵌套属性的变化不会触发。若要侦听所有嵌套的变更,需要使用深层侦听器。
在选项式 API 中,可以这样设置:
export default {watch: {someObject: {handler(newValue, oldValue) {// 注意:在嵌套的变更中,只要没有替换对象本身,那么这里的`newValue`和`oldValue`相同},deep: true}}
}
在组合式 API 中,直接给watch()
传入一个响应式对象,会隐式创建深层侦听器:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {// 在嵌套的属性变更时触发// 注意:`newValue`此处和`oldValue`是相等的,因为它们是同一个对象!
})
obj.count++
如果是返回响应式对象的 getter 函数,默认只有在返回不同对象时才会触发回调,若要强制转成深层侦听器,需显式加上deep
选项:
watch(() => state.someObject,(newValue, oldValue) => {// 注意:`newValue`此处和`oldValue`是相等的,*除非* state.someObject被整个替换了},{ deep: true }
)
注意:深度侦听需要遍历被侦听对象的所有嵌套属性,用于大型数据结构时开销较大,要谨慎使用。
(三)即时回调的侦听器
watch
默认是懒执行的,仅当数据源变化时才执行回调。但有些场景下,我们希望在创建侦听器时立即执行一遍回调,比如请求初始数据。
在选项式 API 中,可以用包含handler
方法和immediate: true
选项的对象来声明侦听器:
export default {//...watch: {question: {handler(newQuestion) {// 在组件实例创建时会立即调用},// 强制立即执行回调immediate: true}}//...
}
在组合式 API 中,通过传入immediate: true
选项来强制回调立即执行:
watch(source,(newValue, oldValue) => {// 立即执行,且当`source`改变时再次执行},{ immediate: true }
)
(四)一次性侦听器(仅支持 3.4 及以上版本)
如果希望侦听器的回调只在源变化时触发一次,可以使用once: true
选项。
在选项式 API 中:
export default {watch: {source: {handler(newValue, oldValue) {// 当`source`变化时,仅触发一次},once: true}}
}
在组合式 API 中:
watch(source,(newValue, oldValue) => {// 当`source`变化时,仅触发一次},{ once: true }
)
四、watchEffect()
函数
watchEffect()
函数可以自动跟踪回调的响应式依赖。例如,用watch
函数加载远程资源的代码:
const todoId = ref(1)
const data = ref(null)
watch(todoId,async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()},{ immediate: true }
)
使用watchEffect()
函数可以简化为:
watchEffect(async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()
})
这里回调会立即执行,不需要指定immediate: true
,并且会自动追踪todoId.value
作为依赖,每当todoId.value
变化时,回调会再次执行。对于有多个依赖项的侦听器,使用watchEffect()
可以避免手动维护依赖列表;在侦听嵌套数据结构中的几个属性时,watchEffect()
可能比深度侦听器更高效,因为它只跟踪回调中使用到的属性。
注意:watchEffect
仅在其同步执行期间追踪依赖,使用异步回调时,只有在第一个await
正常工作前访问到的属性才会被追踪。
(一)watch
与watchEffect
的区别
watch
和watchEffect
都能响应式地执行有副作用的回调,但追踪响应式依赖的方式不同:
watch
只追踪明确侦听的数据源,不会追踪回调中访问到的其他东西,仅在数据源确实改变时才触发回调,能更精确地控制回调函数的触发时机。watchEffect
会在副作用发生期间追踪依赖,在同步执行过程中自动追踪所有能访问到的响应式属性,代码更简洁,但响应性依赖关系可能不那么明确。
五、副作用清理
在侦听器执行异步操作(如异步请求)时,如果在请求完成前数据发生变化,可能会导致使用过时数据。这时可以使用清理函数来解决这个问题。
在 Vue 3.5 + 版本中,可以使用onWatcherCleanup()
API 来注册清理函数,在侦听器失效并准备重新运行时被调用:
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {const controller = new AbortController()fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {// 回调逻辑})onWatcherCleanup(() => {// 终止过期请求controller.abort()})
})
在 3.5 之前的版本,以及在 Vue 3.5 + 版本中,还可以通过将onCleanup
函数作为第三个参数传递给侦听器回调和watchEffect
作用函数的第一个参数来实现清理功能:
watch(id, (newId, oldId, onCleanup) => {//...onCleanup(() => {// 清理逻辑})
})
watchEffect((onCleanup) => {//...onCleanup(() => {// 清理逻辑})
})
六、回调的触发时机
当更改响应式状态时,可能会同时触发 Vue 组件更新和侦听器回调。默认情况下,侦听器回调会在父组件更新(如有)之后、所属组件的 DOM 更新之前被调用,此时在侦听器回调中访问所属组件的 DOM,得到的是更新前的状态。
(一)Post Watchers
如果想在侦听器回调中访问被 Vue 更新之后的所属组件的 DOM,需要指明flush: 'post'
选项。
在选项式 API 中:
export default {//...watch: {key: {handler() {},flush: 'post'}}
}
在组合式 API 中:
watch(source, callback, {flush: 'post'
})
watchEffect(callback, {flush: 'post'
})
后置刷新的watchEffect()
有个更方便的别名watchPostEffect()
:
import { watchPostEffect } from 'vue'
watchPostEffect(() => {/* 在Vue更新后执行 */
})
(二)同步侦听器
还可以创建同步触发的侦听器,它会在 Vue 进行任何更新之前触发。
在选项式 API 中:
export default {//...watch: {key: {handler() {},flush: 'sync'}}
}
同步触发的watchEffect()
有个更方便的别名watchSyncEffect()
:
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {/* 在响应式数据变化时同步执行 */
})
注意:同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发,适合监视简单的布尔值,应避免在可能多次同步修改的数据源(如数组)上使用。
七、this.$watch()
方法
在组件实例中,还可以使用this.$watch()
方法命令式地创建一个侦听器:
export default {created() {this.$watch('question', (newQuestion) => {//...})}
}
这种方式在特定条件下设置侦听器,或者只侦听响应用户交互的内容时很有用,并且可以提前停止该侦听器。
八、停止侦听器
用watch
选项或者this.$watch()
实例方法声明的侦听器,会在宿主组件卸载时自动停止。但在少数情况下,需要在组件卸载前手动停止侦听器,可以调用this.$watch()
API 返回的函数:
const unwatch = this.$watch('foo', callback)
//...当该侦听器不再需要时
unwatch()
在setup()
或<script setup>
中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,在宿主组件卸载时自动停止。但如果用异步回调创建侦听器,它不会绑定到当前组件上,必须手动停止,否则可能造成内存泄漏。例如:
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
//...这个则不会!
setTimeout(() => {watchEffect(() => {})
}, 100)
</script>
手动停止侦听器的方法是调用watch
或watchEffect
返回的函数:
const unwatch = watchEffect(() => {})
//...当该侦听器不再需要时
unwatch()
尽量选择同步创建侦听器,如果需要等待异步数据,可以使用条件式的侦听逻辑,如:
// 需要异步请求得到的数据
const data = ref(null)
watchEffect(() => {if (data.value) {// 数据加载后执行某些操作...}
})