vue3之数据监听

3,368 阅读10分钟

一、前言

对于一个vue的老玩家来说,数据的监听可能再熟悉不过了,在平时的开发中,或多或少都会使用watch和去监听某个属性,而数据的监听也是组件的一项重要工作,在vue3中,虽然保留了这个API,但是变化还是很大的,而这篇文章,就是要讲解着watch的变化与使用,希望对你有帮助。

二、watch

对于watchvue3在保留了原来的功能之外,还新增了watchEffect来帮助我们更简单的进行监听。

回顾vue2

首先先回顾下vue2中的用法, 在vue2中,watch是一个对象,和datamethods同级配置:

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')
    }
  }
}

说实话,对于初学者来说可能不太友好,有点复杂了,另外,不能用箭头函数来定义watcher函数,因为箭头函数绑定了父级作用域的上下文,所以this不能指向组件实例。

同时,vue2也可以通过this.$watch()这个API的用法来实现对某个数据的监听,它接受三个参数: sourcecallbackoptions

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

拥抱vue3

vue3的组合式API中,watch是一个函数,可以接受三个参数;

import { watch } from 'vue'

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

类型声明

在学习它的用法之前,我们先来对它的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
  1. 批量监听的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)[];
// ...

无论是基础用法还是批量监听,他都接受三个参数:

参数是否必传含义
source必传数据源
callback必传监听到变化要执行的回调函数
options可选监听选项

监听的数据源

为了避免出现监听了但是没反应的情况出现,我们需要先对数据源的TS类型和使用限制做下了解:

// source的 TS 类型
export declare type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
// ...

可以看出,能够用于监听的对象有通过响应式API定义的变量(Ref<T>),计算数据(computedRef<T>),和getter函数(() => T)

可以看出,想要成功的监听一个数据,数据源必须是具备响应性的或是一个getter,所以只是通let定义的普通变量,是无法监听到他的改变的。

如果是一个响应式的对象,即它本身是响应式的,但是他的prototype不是,如果想要监听他的某个值,就需要写成一个getter函数,即一个有返回值的函数,返回你要监听的属性,如() => foo.bar

回调函数

watchAPI的第二个参数是一个回调函数,是监听到变化之后的行为,他的TS类型如下:

export declare type WatchCallback<V = any, OV = any> = (
  value: V,
  oldValue: OV,
  onCleanup: OnCleanup
) => any
// ...

可以看出,回调函数也有三个参数,分别是新值value,旧值oldValue,清理函数onCleanup,这三个参数都可以自己命名,比如把value换成newValue什么的。

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

介绍完了前两个参数之后,我们可以先来学习下基础用法,也是我们日常中最常用的方法:

<script lang="ts">
import { defineComponent, reactive, watch } from 'vue'

interface UserInfo {
  name: string
  age: number
}
export default defineComponent({
  setup() {
    // 定义一个响应式变量
    const userInfo: UserInfo = reactive({
      name: 'Tom',
      age: 18,
    })

    // 3秒后改变这个变量
    setTimeout(() => {
      userInfo.name = 'Tony'
    }, 3000)

    // 监听整个对象
    watch(userInfo, (newValue, oldValue) => {
      console.log('监听了整个对象 ', newValue)
      console.log('监听了整个对象 ', oldValue)
    })

    // 监听某个属性
    watch(
      () => userInfo.name,
      (newValue, oldValue) => {
        console.log('只监听了name:', newValue)
        console.log('只监听了name:', oldValue)
      }
    )
  },
})
</script>

watch.gif

可以看到,数据监听成功了,但是你应该发现了,监听对象时,newValueoldValue的值是一样的,因为他们指向的是同一个对象,不仅仅是对象,只要是引用类型,都是如此。

监听多个对象

watch除了能监听单个数据之外,还可以同时监听多个数据,局别在于他是将多个数据源和回调函数的参数都变成了数组的形式,看下面的例子:

<script lang="ts">
import { defineComponent, reactive, watch, ref } from 'vue'

interface UserInfo {
  name: string
  age: number
}
export default defineComponent({
  setup() {
    // 定义两个响应式变量
    const userInfo: UserInfo = reactive({
      name: 'Tom',
      age: 18,
    })

    const count = ref<number>(0)

    // 3秒后改变变量
    setTimeout(() => {
      userInfo.name = 'Tony'
      count.value++
    }, 3000)

    // 批量监听
    watch(
      [() => userInfo.name, count],
      ([newUserName, newCount], [oldUserName, oldCount]) => {
        console.log(newUserName, oldUserName)
        console.log(newCount, oldCount)
      }
    )
  },
})
</script>

watch.gif

以上就是批量监听的用法了。当我们有多个数据的变化都做同一件事时就可以使用这个功能啦。

监听的选项

讲完了监听的用法之后,我们来讲一下监听的选项,首先介绍在他的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
}

watch的第三个参数是一个对象,可选以下几个选项:

选项类型默认值可选值作用
deepbooleanfalsetrue、false是否进行深度监听
immediatebooleanfalsetrue、false是否立即执行监听回调
flushstring'pre''pre'、'post'、'sync'控制监听回调的调用时机
onTrack(e) => void在数据源被追踪时调用
onTrigger(e) => void在监听回调被触发时调用

其中onTrackonTriggeredebugger事件,建议在回调内方式一个debugger语句以调试依赖,且这两个选项仅在开发模式有效。

监听选项之deep

deep选项是一个布尔值,设置为true时开启深度监听,或者是false关闭深度监听,看例子:

<script lang="ts">
import { defineComponent, watch, ref } from 'vue'

export default defineComponent({
  setup() {
    // 定义一个响应式变量

    const numbers = ref<number[]>([0])

    // 3秒后改变这个变量
    setTimeout(() => {
      numbers.value.push(1)
      console.log('修改后的值:', numbers.value)
    }, 3000)

    // 监听数组
    watch(
      numbers,
      () => {
        console.log('监听后的值:', numbers.value)
      },
      {
        deep: true, // 只有当deep为true时才能监听到数组的改变
      }
    )
  },
})
</script>

上面的例子使用ref定义了一个数组变量。然后在3s后改变数组的值,你会发现,只有当deeptrue时才能出发监听,需要注意的是,使用reactive声明的变量,deep是默认为true的。

监听选项之immediate

watch默认是惰性的,只有当监听的数据发生变化时才会执行回调;

<script lang="ts">
import { defineComponent, watch, ref } from 'vue'

export default defineComponent({
  setup() {
    // 当immediate为true时触发
    const msg = ref<string>('Hello')

    // 3秒后改变这个变量
    setTimeout(() => {
      msg.value = 'Hello vue3'
    }, 3000)

    // 监听整个对象
    watch(
      msg,
      () => {
        console.log('监听后的值:', msg.value)
      },
      {
        immediate: false,
      }
    )
  },
})
</script>

上面这个例子,当immediate的值为true时,控制台会打印改变前后的msg值,就是说,只有当immediatetrue时,才会在初始化变量的时候就触发回调函数,否则的话默认是只有当数据源变化时才会触发监听。

监听选项之flush

flush选项是用来控制监听回调的调用时机,接受三个指定的字符串,分别是'pre''post''sync',默认是'pre'

回调的调用时机使用场景
pre将在渲染前被调用允许回调在模板运行前更新了其他值
post被推迟到渲染之后调用如果要通过ref操作DOM元素或子组件,需要使用改值来启用改选项以达到预期的执行效果
sync在渲染时同步调用目前来说没什么好处,可以了解但不建议使用

停止监听

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

但是如果我们需要手动取消监听怎么办呢?比如说我们在一个异步函数里面创建监听函数,这个时候监听函数就不会绑定到当前组件,因此组件卸载的时候就不会一起停止侦听器,这个时候就需要手动停止监听。别担心,vue3给我们提供了方法。

老规矩,先看下他的TS类型:

export declare type WatchStopHandle = () => void;

用法很简单,如下:

// 定义一个取消观察的变量,它是一个函数
const unwatch = watch(message, () => {
  // ...
})

// 在合适的时期调用它,可以取消这个监听
unwatch()

不过需要注意的是,如果你启用了immediate选项,是不能在第一次触发监听的时候执行它;

<script lang="ts">
import { defineComponent, watch, ref } from 'vue'

export default defineComponent({
  setup() {
    // 定义一个响应式变量

    const msg = ref<string>('Hello')

    // 3秒后改变这个变量
    setTimeout(() => {
      msg.value = 'Hello vue3'
    }, 3000)

    // 监听整个对象
    const unwatch = watch(
      msg,
      () => {
        console.log('监听后的值:', msg.value)
        unwatch() // 开启了immediate时不能在这里取消监听,会报错
      },
      {
        immediate: true,
      }
    )
  },
})
</script>

上面的代码会出现如下的报错

Uncaught (in promise) ReferenceError: Cannot access 'unwatch' before initialization

解决的方案也很很简单,可以使用let并判断变量类型;

import { defineComponent, watch, ref } from 'vue'
import type { WatchStopHandle } from 'vue'

export default defineComponent({
  setup() {
    // 定义一个响应式变量

    const msg = ref<string>('Hello')

    // 3秒后改变这个变量
    setTimeout(() => {
      msg.value = 'Hello vue3'
    }, 3000)

    // 需要另起一行,并增加类型
    let unwatch: WatchStopHandle
    unwatch = watch(
      msg,
      () => {
        console.log('监听后的值:', msg.value)
        // 加一个判断,是函数才执行它
        if (typeof unwatch === 'function') {
          unwatch()
        }
      },
      {
        immediate: true,
      }
    )
  },
})
</script>

监听效果的清理

在前面介绍回调函数的时候提到过第三个参数,onCleanup,它可以帮我们注册一个清理函数。

有时候watch的回调会执行异步操作,当watch到数据变更的时候,需要取消这些操作,这个时候这个参数就派上用场了,使用场景如下:

  • watcher即将重新运行的时候
  • watcher被停止(组件被现在或者手动停止监听)

TS类型

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

用法也很简单,传入一个回调函数运行即可,不过需要注意的是,需要在停止监听之前注册号清理行为,否则是不会生效的; 我们继续使用上面的例子介绍清理函数的使用方法;

<script lang="ts">
import { defineComponent, watch, ref } from 'vue'
import type { WatchStopHandle } from 'vue'

export default defineComponent({
  setup() {
    // 定义一个响应式变量

    const msg = ref<string>('Hello')

    // 3秒后改变这个变量
    setTimeout(() => {
      msg.value = 'Hello vue3'
    }, 3000)

    // 需要另起一行,并增加类型
    let unwatch: WatchStopHandle
    unwatch = watch(
      msg,
      (newVal, oldVal, onCleanup) => {
        console.log('监听后的值:', msg.value)
        // 一定要在停止监听函数执行前执行清理函数
        onCleanup(() => {
          console.log('执行了清理函数。。。')
        })
        // 然后在停止监听
        if (typeof unwatch === 'function') {
          unwatch()
        }
      },
      {
        immediate: true,
      }
    )
  },
})
</script>

三、watchEffect

vue3中,如果我们的某个函数包含了多个需要监听的数据,我们可以使用watchEffect来简化操作

TS类型

这个API的类型如下,使用的时候需要传入一个副作用函数,相当于watch的回调函数,也可以传入一些可选的选项。同时它也会返回一个用于停止监听的函数;


export declare type WatchEffect = (onCleanup: OnCleanup) => void

export declare function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle

可以看到,他也会传入一个清理回调的参数onCleanup,用法和上面是一样的;

使用示例

<script lang="ts">
import { defineComponent, watchEffect, ref } from 'vue'

export default defineComponent({
  setup() {
    // 定义一个响应式变量

    const name = ref<string>('Tom')
    const age = ref<number>(18)

    const userInfo = (): void => {
      console.table({
        name: name.value,
        age: age.value,
      })
    }
    // 3秒后改变name
    setTimeout(() => {
      name.value = 'Jeck'
    }, 3000)

    // 4秒后改变age
    setTimeout(() => {
      age.value = 22
    }, 4000)

    watchEffect(userInfo)
  },
})
</script>

watch.gif

watch的区别:

根据上面的例子,我们可以看出他们还是有一定区别的;

  1. watch可以访问侦听数据变化前后的值,而watchEffect没有;

  2. watch是属性变化之后才执行,而watchEffect则会默认执行一次,然后在变化时执行;

四、总结

以上,便是vue3中数据监听的用法了,中的来说,相比于vue2,还是有一个比较大的改变的,功能也更强大了,同时还增加了批量监听的APIwatchEffect,为我们以后的数据监听带来了很大的方便,同时,数据监听也是我们在日常开发中很常用的功能,所以对于watch的使用,一定要很熟悉才行,文章还是很基础的,需要对大家有用,同时,如果有不对的地方,欢迎大家指正,小弟感激不尽!!!