首页IT科技vue监听数据变化原理(VUE3 数据的侦听)

vue监听数据变化原理(VUE3 数据的侦听)

时间2025-05-02 16:23:05分类IT科技浏览4121
导读:侦听数据变化也是组件里的一项重要工作,比如侦听路由变化、侦听参数变化等等。...

侦听数据变化也是组件里的一项重要工作            ,比如侦听路由变化            、侦听参数变化等等            。

Vue 3 在保留原来的 watch 功能之外                  ,还新增了一个 watchEffect 帮助更简单的进行侦听                  。

watch

在 Vue 3      ,新版的 watch 和 Vue 2 的旧版写法对比      ,在使用方式上变化非常大!

回顾 Vue 2 在 Vue 2 是这样用的                  ,和 data                  、 methods 都在同级配置: export default { data() { return { // ... } }, // 注意这里            ,放在 `data`       、 `methods` 同个级别 watch: { // ... }, methods: { // ... }, }

并且类型繁多      ,选项式 API 的类型如下:

watch: { [key: string]: string | Function | Object | Array}

联合类型过多                  ,意味着用法复杂            ,下面是个很好的例子,虽然出自 官网 的用法介绍                  ,但过于繁多的用法也反映出来对初学者不太友好                  ,初次接触可能会觉得一头雾水:

export default { data() { return { a: 1, b: 2, c: { d: 4, }, e: 5, f: 6, } }, watch: { // 侦听顶级 Property a(val, oldVal) { console.log(`new: ${val}, old: ${oldVal}`) }, // 字符串方法名 b: someMethod, // 该回调会在任何被侦听的对象的 Property 改变时被调用,不论其被嵌套多深 c: { handler(val, oldVal) { console.log(c changed) }, deep: true, }, // 侦听单个嵌套 Property c.d: function (val, oldVal) { // do something }, // 该回调将会在侦听开始之后被立即调用 e: { handler(val, oldVal) { console.log(e changed) }, immediate: true, }, // 可以传入回调数组            ,它们会被逐一调用 f: [ handle1, function handle2(val, oldVal) { console.log(handle2 triggered) }, { handler: function handle3(val, oldVal) { console.log(handle3 triggered) }, /* ... */ }, ], }, methods: { someMethod() { console.log(b changed) }, handle1() { console.log(handle 1 triggered) }, }, }

当然肯定也会有开发者会觉得这样选择多是个好事                  ,选择适合自己的就好      ,但笔者还是认为这种写法对于初学者来说不是那么友好            ,有些过于复杂化                  ,如果一个用法可以适应各种各样的场景      ,岂不是更妙?

TIP 另外需要注意的是      ,不能使用箭头函数来定义 Watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue) )      。 因为箭头函数绑定了父级作用域的上下文                  ,所以 this 将不会按照期望指向组件实例            , this.updateAutocomplete 将是 undefined       。

Vue 2 也可以通过 this.$watch() 这个 API 的用法来实现对某个数据的侦听      ,它接受三个参数: source             、 callback 和 options                   。

export default { data() { return { a: 1, } }, // 生命周期钩子 mounted() { this.$watch(a, (newVal, oldVal) => { // ... }) }, }

由于 this.$watch 的用法和 Vue 3 比较接近                  ,所以这里不做过多的回顾            ,请直接看 了解 Vue 3 部分            。

了解 Vue 3 在 Vue 3 的组合式 API 写法, watch 是一个可以接受 3 个参数的函数(保留了 Vue 2 的 this.$watch 这种用法)                  ,在使用层面上简单了很多      。

import { watch } from vue // 一个用法走天下 watch( source, // 必传                  ,要侦听的数据源 callback // 必传,侦听到变化后要执行的回调函数 // options // 可选            ,一些侦听选项 )

下面的内容都基于 Vue 3 的组合式 API 用法展开讲解                  。

API 的 TS 类型

在了解用法之前                  ,先对它的 TS 类型声明做一个简单的了解      , watch 作为组合式 API             ,根据使用方式有两种类型声明:

1.基础用法的 TS 类型                  ,详见 基础用法 部分 // watch 部分的 TS 类型 // ... export declare function watch<T, Immediate extends Readonly<boolean> = false>( source: WatchSource<T>, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate> ): WatchStopHandle // ...

2.批量侦听的 TS 类型      ,详见 批量侦听 部分

// watch 部分的 TS 类型 // ... export declare function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false >( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate> ): WatchStopHandle // MultiWatchSources 是一个数组 declare type MultiWatchSources = (WatchSource<unknown> | object)[] // ...

但是不管是基础用法还是批量侦听      ,可以看到这个 API 都是接受三个入参:

并返回一个可以用来停止侦听的函数(详见:停止侦听)            。

要侦听的数据源 在上面 API 的 TS 类型 已经对 watch API 的组成有一定的了解了                  ,这里先对数据源的类型和使用限制做下说明。 TIP 如果不提前了解            ,在使用的过程中可能会遇到 “侦听了但没有反应            ” 的情况出现                  。 另外      ,这部分内容会先围绕基础用法展开说明                  ,批量侦听会在 批量侦听 部分单独说明                  。

watch API 的第 1 个参数 source 是要侦听的数据源            ,它的 TS 类型如下:

// watch 第 1 个入参的 TS 类型 // ... export declare type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T) // ...

可以看到能够用于侦听的数据,是通过 响应式 API 定义的变量( Ref )                  ,或者是一个 计算数据 ( ComputedRef )                  ,或者是一个 getter 函数 ( () => T )。

所以要想定义的 watch 能够做出预期的行为,数据源必须具备响应性或者是一个 getter             ,如果只是通过 let 定义一个普通变量                  ,然后去改变这个变量的值      ,这样是无法侦听的            。

TIP 如果要侦听响应式对象里面的某个值(这种情况下对象本身是响应式            , 但它的 property 不是)                  , 需要写成 getter 函数      , 简单的说就是需要写成有返回值的函数      , 这个函数 return 要侦听的数据                  , e.g. () => foo.bar             , 可以结合下方 基础用法 的例子一起理解                  。

侦听后的回调函数 在上面 API 的 TS 类型 介绍了 watch API 的组成      ,和数据源一样                  ,先了解一下回调函数的定义      。

TIP 和数据源部分一样            ,回调函数的内容也是会先围绕基础用法展开说明, 批量侦听会在 批量侦听 部分单独说明            。

watch API 的第 2 个参数 callback 是侦听到数据变化时要做出的行为                  ,它的 TS 类型如下:

// watch 第 2 个入参的 TS 类型 // ... export declare type WatchCallback<V = any, OV = any> = ( value: V, oldValue: OV, onCleanup: OnCleanup ) => any // ...

乍一看它有三个参数                  ,但实际上这些参数不是自己定义的,而是 watch API 传给的            ,所以不管用或者不用                  ,它们都在那里:

注意:第一个参数是新值      ,第二个才是原来的旧值!

如同其他 JS 函数            ,在使用 watch 的回调函数时                  ,可以对这三个参数任意命名      ,比如把 value 命名为觉得更容易理解的 newValue                   。

TIP 如果侦听的数据源是一个 引用类型 时( e.g. Object                   、 Array       、 Date … )      , value 和 oldValue 是完全相同的                  ,因为指向同一个对象      。

另外            ,默认情况下      ,watch 是惰性的                  ,也就是只有当被侦听的数据源发生变化时才执行回调      。

基础用法 来到这里            ,对 2 个必传的参数都有一定的了解了,先看看基础的用法                  ,也就是日常最常编写的方案                  ,只需要先关注前 2 个必传的参数                  。

// 不要忘了导入要用的 API import {defineComponent,reactive ,watch} from vue export default defineComponent({ setup(){ //定义一个响应式数据 const userInfo=reactive({ name:Petter, age:18 }) //2s后改变数据 setTimeout(()=>{ userInfo.name=tom },2000) /** * 可以直接侦听这个响应式对象 * callback 的参数如果不用可以不写 */ watch(userInfo,()=>{ console.log(侦听整个 userInfo , userInfo.name) }) /** * 也可以侦听对象里面的某个值 * 此时数据源需要写成 getter 函数 */ watch( //数据源,getter形式 ()=>userInfo.name, // 回调函数 callback (newValue, oldValue) => { console.log(只侦听 name 的变化 , userInfo.name) console.log(打印变化前后的值, { oldValue, newValue }) } ) } })

一般的业务场景,基础用法足以面对            。

如果有多个数据源要侦听            ,并且侦听到变化后要执行的行为一样                  ,那么可以使用 批量侦听       。

特殊的情况下      ,可以搭配 侦听的选项 做一些特殊的用法            ,详见下面部分的内容                  。

批量侦听

如果有多个数据源要侦听                  ,并且侦听到变化后要执行的行为一样      ,第一反应可能是这样来写:

1.抽离相同的处理行为为公共函数

2.然后定义多个侦听操作      ,传入这个公共函数 import { defineComponent, ref, watch } from vue export default defineComponent({ setup() { const message = ref<string>() const index = ref<number>(0) // 2s后改变数据 setTimeout(() => { // 来到这里才会触发 watch 的回调 message.value = Hello World! index.value++ }, 2000) // 抽离相同的处理行为为公共函数 const handleWatch = ( newValue: string | number, oldValue: string | number ): void => { console.log({ newValue, oldValue }) } // 然后定义多个侦听操作                  ,传入这个公共函数 watch(message, handleWatch) watch(index, handleWatch) }, })

这样写其实没什么问题            ,不过除了抽离公共代码的写法之外      , watch API 还提供了一个批量侦听的用法                  ,和 基础用法 的区别在于            ,数据源和回调参数都变成了数组的形式            。

数据源:以数组的形式传入,里面每一项都是一个响应式数据。

回调参数:原来的 value 和 newValue 也都变成了数组                  ,每个数组里面的顺序和数据源数组排序一致                  。

可以看下面的这个例子更为直观:

import { defineComponent, ref, watch } from vue export default defineComponent({ setup(){ //定义多个数据源 const message = ref<string>() const index = ref<number>(0) //2s后改变数据 setTimeout(()=>{ message.value = Hello World! index.value++ },2000) watch( //数据源改成了数组 [message, index], //回调的入参也变成了数组                  ,每个数组里面的顺序和数据源数组排序一致 ([newMessage, newIndex], [oldMessage, oldIndex])=>{ console.log(message 的变化, { newMessage, oldMessage }) console.log(index 的变化, { newIndex, oldIndex }) } ) } })

什么情况下可能会用到批量侦听呢?比如一个子组件有多个 props ,当有任意一个 prop 发生变化时            ,都需要执行初始化函数重置组件的状态                  ,那么这个时候就可以用上这个功能啦!

TIP 在适当的业务场景      , 也可以使用 watchEffect 来完成批量侦听            , 但请留意 功能区别 部分的说明                  。

侦听的选项

在 API 的 TS 类型 里提到                  , watch API 还接受第 3 个参数 options      ,可选的一些侦听选项。

它的 TS 类型如下: // watch 第 3 个入参的 TS 类型 // ... export declare interface WatchOptions<Immediate = boolean> extends WatchOptionsBase { immediate?: Immediate deep?: boolean } // ... // 继承的 base 类型 export declare interface WatchOptionsBase extends DebuggerOptions { flush?: pre | post | sync } // ... // 继承的 debugger 选项类型 export declare interface DebuggerOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void } // ...

options 是一个对象的形式传入      ,有以下几个选项:

其中 onTrack 和 onTrigger 的 e 是 debugger 事件                  ,建议在回调内放置一个 debugger 语句 以调试依赖            ,这两个选项仅在开发模式下生效            。 TIP deep 默认是 false       , 但是在侦听 reactive 对象或数组时                  ,会默认为 true             , 详见 侦听选项之 deep                  。

侦听选项之 deep deep 选项接受一个布尔值,可以设置为 true 开启深度侦听                  ,或者是 false 关闭深度侦听                  ,默认情况下这个选项是 false 关闭深度侦听的,但也存在特例      。

设置为 false 的情况下            ,如果直接侦听一个响应式的 引用类型 数据(e.g. Object       、 Array … )                  ,虽然它的属性的值有变化      ,但对其本身来说是不变的            ,所以不会触发 watch 的 callback             。

下面是一个关闭了深度侦听的例子:

import { defineComponent, ref, watch } from vue export default defineComponent({ setup() { // 定义一个响应式数据                  ,注意用的是 ref 来定义 const nums = ref<number[]>([]) // 2s后给这个数组添加项目 setTimeout(() => { nums.value.push(1) // 可以打印一下      ,确保数据确实变化了 console.log(修改后, nums.value) }, 2000) // 但是这个 watch 不会按预期执行 watch( nums, // 这里的 callback 不会被触发 () => { console.log(触发侦听, nums.value) }, // 因为关闭了 deep { deep: false, } ) }, })

类似这种情况      ,需要把 deep 设置为 true 才可以触发侦听                  。

可以看到上面的例子特地用了 ref API                   ,这是因为通过 reactive API 定义的对象无法将 deep 成功设置为 false (这一点在目前的官网文档未找到说明            ,最终是在 watch API 的源码 上找到了答案)      。

// ... if (isReactive(source)) { getter = () => source deep = true // 被强制开启了 } // ...

这个情况就是上面所说的 “特例                  ”       ,可以通过 isReactive API 来判断是否需要手动开启深度侦听      。

// 导入 isReactive API import { defineComponent, isReactive, reactive, ref } from vue export default defineComponent({ setup() { // 侦听这个数据时                  ,会默认开启深度侦听 const foo = reactive({ name: Petter, age: 18, }) console.log(isReactive(foo)) // true // 侦听这个数据时            ,不会默认开启深度侦听 const bar = ref({ name: Petter, age: 18, }) console.log(isReactive(bar)) // false }, })

侦听选项之 immediate 在 侦听后的回调函数 部分有了解过, watch 默认是惰性的                  ,也就是只有当被侦听的数据源发生变化时才执行回调                  。

这句话是什么意思呢?来看一下这段代码                  ,为了减少 deep 选项的干扰,换一个类型            ,换成 string 数据来演示                  ,请留意注释:

import { defineComponent, ref, watch } from vue export default defineComponent({ setup() { // 这个时候不会触发 watch 的回调 const message = ref<string>() // 2s后改变数据 setTimeout(() => { // 来到这里才会触发 watch 的回调 message.value = Hello World! }, 2000) watch(message, () => { console.log(触发侦听, message.value) }) }, })

可以看到      ,数据在初始化的时候并不会触发侦听回调            ,如果有需要的话                  ,通过 immediate 选项来让它直接触发            。

immediate 选项接受一个布尔值      ,默认是 false       ,可以设置为 true 让回调立即执行      。

改成这样                  ,请留意高亮的代码部分和新的注释:

import { defineComponent, ref, watch } from vue export default defineComponent({ setup() { // 这一次在这里可以会触发 watch 的回调了 const message = ref<string>() // 2s后改变数据 setTimeout(() => { // 这一次            ,这里是第二次触发 watch 的回调      ,不再是第一次 message.value = Hello World! }, 2000) watch( message, () => { console.log(触发侦听, message.value) }, // 设置 immediate 选项 { immediate: true, } ) }, })

注意                  ,在带有 immediate 选项时            ,不能在第一次回调时取消该数据源的侦听,详见 停止侦听 部分                  。

侦听选项之 flush

flush 选项是用来控制 侦听回调 的调用时机                  ,接受指定的字符串                  ,可选值如下,默认是 ‘pre’             。

对于 ‘pre’ 和 ‘post’             ,回调使用队列进行缓冲。回调只被添加到队列中一次                  。

即使观察值变化了多次                  ,值的中间变化将被跳过      ,不会传递给回调            ,这样做不仅可以提高性能                  ,还有助于保证数据的一致性                  。

更多关于 flush 的信息      ,请参阅 回调的触发时机 。

停止侦听 如果在 setup 或者 script-setup 里使用 watch 的话      , 组件被卸载 的时候也会一起被停止                  ,一般情况下不太需要关心如何停止侦听            。

不过有时候可能想要手动取消            , Vue 3 也提供了方法                  。

TIP 随着组件被卸载一起停止的前提是      ,侦听器必须是 同步语句 创建的                  , 这种情况下侦听器会绑定在当前组件上      。 如果放在 setTimeout 等 异步函数 里面创建            , 则不会绑定到当前组件,因此组件卸载的时候不会一起停止该侦听器                  , 这种时候就需要手动停止侦听            。

在 API 的 TS 类型 有提到                  ,当在定义一个 watch 行为的时候,它会返回一个用来停止侦听的函数                  。

这个函数的 TS 类型如下:

export declare type WatchStopHandle = () => void

用法很简单            ,做一下简单了解即可:

// 定义一个取消观察的变量                  ,它是一个函数 const unwatch = watch(message, () => { // ... }) // 在合适的时期调用它      ,可以取消这个侦听 unwatch()

但是也有一点需要注意的是            ,如果启用了 immediate 选项                   ,不能在第一次触发侦听回调时执行它      。

// 注意:这是一段错误的代码      ,运行会报错 const unwatch = watch( message, // 侦听的回调 () => { // ... // 在这里调用会有问题 ❌ unwatch() }, // 启用 immediate 选项 { immediate: true, } )

会收获一段报错      ,告诉 unwatch 这个变量在初始化前无法被访问:

Uncaught ReferenceError: Cannot access unwatch before initialization

目前有两种方案可以让实现这个操作:

方案一:使用 var 并判断变量类型                  ,利用 var 的变量提升 来实现目的      。

// 这里改成 var             ,不要用 const 或 let var unwatch = watch( message, // 侦听回调 () => { // 这里加一个判断      ,是函数才执行它 if (typeof unwatch === function) { unwatch() } }, // 侦听选项 { immediate: true, } )

不过 var 已经属于过时的语句了                  ,建议用方案二的 let                   。

方案二:使用 let 并判断变量类型            。

// 如果不想用 any             ,可以导入 TS 类型 import type { WatchStopHandle } from vue // 这里改成 let ,但是要另起一行                  ,先定义                  ,再赋值 let unwatch: WatchStopHandle unwatch = watch( message, // 侦听回调 () => { // 这里加一个判断,是函数才执行它 if (typeof unwatch === function) { unwatch() } }, // 侦听选项 { immediate: true, } )

侦听效果清理 在 侦听后的回调函数 部分提及到一个参数 onCleanup             ,它可以帮注册一个清理函数      。

有时 watch 的回调会执行异步操作                  ,当 watch 到数据变更的时候      ,需要取消这些操作            ,这个函数的作用就用于此                  ,会在以下情况调用这个清理函数:

watcher 即将重新运行的时候

watcher 被停止(组件被卸载或者被手动 停止侦听 )

TS 类型:

declare type OnCleanup = (cleanupFn: () => void) => void

用法方面比较简单      ,传入一个回调函数运行即可      ,不过需要注意的是                  ,需要在停止侦听之前注册好清理行为            ,否则不会生效                  。

在 停止侦听 里的最后一个 immediate 例子的基础上继续添加代码      ,请注意注册的时机:

let unwatch: WatchStopHandle unwatch = watch( message, (newValue, oldValue, onCleanup) => { // 需要在停止侦听之前注册好清理行为 onCleanup(() => { console.log(侦听清理ing) // 根据实际的业务情况定义一些清理操作 ... }) // 然后再停止侦听 if (typeof unwatch === function) { unwatch() } }, { immediate: true, } )

watchEffect 如果一个函数里包含了多个需要侦听的数据                  ,一个一个数据去侦听太麻烦了            ,在 Vue 3 ,可以直接使用 watchEffect API 来简化的操作            。

API 的 TS 类型

这个 API 的类型如下                  ,使用的时候需要传入一个副作用函数(相当于 watch 的 侦听后的回调函数 )                  ,也可以根据的实际情况传入一些可选的 侦听选项 。

和 watch API 一样,它也会返回一个用于 停止侦听 的函数                  。

// watchEffect 部分的 TS 类型 // ... export declare type WatchEffect = (onCleanup: OnCleanup) => void export declare function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle // ...

副作用函数也会传入一个清理回调作为参数            ,和 watch 的 侦听效果清理 一样的用法                  。

可以理解为它是一个简化版的 watch                   ,具体简化在哪里呢?请看下面的用法示例。

用法示例

它立即执行传入的一个函数      ,同时响应式追踪其依赖            ,并在其依赖变更时重新运行该函数            。

import { defineComponent, ref, watchEffect } from vue export default defineComponent({ setup() { // 单独定义两个数据                  ,后面用来分开改变数值 const name = ref<string>(Petter) const age = ref<number>(18) // 定义一个调用这两个数据的函数 const getUserInfo = (): void => { console.log({ name: name.value, age: age.value, }) } // 2s后改变第一个数据 setTimeout(() => { name.value = Tom }, 2000) // 4s后改变第二个数据 setTimeout(() => { age.value = 20 }, 4000) // 直接侦听调用函数      ,在每个数据产生变化的时候      ,它都会自动执行 watchEffect(getUserInfo) }, })

和 watch 的区别

虽然理论上 watchEffect 是 watch 的一个简化操作                  ,可以用来代替 批量侦听             ,但它们也有一定的区别:

1.watch 可以访问侦听状态变化前后的值      ,而 watchEffect 没有                  。

2.watch 是在属性改变的时候才执行                  ,而 watchEffect 则默认会执行一次            ,然后在属性改变的时候也会执行      。

第二点的意思,看下面这段代码可以有更直观的理解:

使用 watch :

export default defineComponent({ setup() { const foo = ref<string>() setTimeout(() => { foo.value = Hello World! }, 2000) function bar() { console.log(foo.value) } // 使用 watch 需要先手动执行一次 bar() // 然后当 foo 有变动时                  ,才会通过 watch 来执行 bar() watch(foo, bar) }, })

使用 watchEffect :

export default defineComponent({ setup() { const foo = ref<string>() setTimeout(() => { foo.value = Hello World! }, 2000) function bar() { console.log(foo.value) } // 可以通过 watchEffect 实现 bar() + watch(foo, bar) 的效果 watchEffect(bar) }, })

可用的侦听选项 虽然用法和 watch 类似                  ,但也简化了一些选项,它的侦听选项 TS 类型如下:

// 只支持 base 类型 export declare interface WatchOptionsBase extends DebuggerOptions { flush?: pre | post | sync } // ... // 继承的 debugger 选项类型 export declare interface DebuggerOptions { onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void } // ...

对比 watch API             ,它不支持 deep 和 immediate                   ,请记住这一点      ,其他的用法是一样的            。

flush 选项的使用详见 侦听选项之 flush             ,onTrack 和 onTrigger 详见 侦听的选项 部分内容                  。

watchPostEffect watchEffect API 使用 flush: ‘post’ 选项时的别名                  ,具体区别详见 侦听选项之 flush 部分      。

TIP Vue v3.2.0 及以上版本才支持该 API       。

watchSyncEffect watchEffect API 使用 flush: ‘sync’ 选项时的别名      ,具体区别详见 侦听选项之 flush 部分                  。

TIP Vue v3.2.0 及以上版本才支持该 API             。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
关键词创意撰写案例(创作关键词生成器——让创作更高效) js object深拷贝(JSON.stringify Function (JavaScript))