Listening for VUE3 data

Listening to data changes is also an important task in components, such as listening to routing changes, listening to parameter changes, and so on.

In addition to retaining the original watch function, Vue 3 also adds a new watchEffect to help listen more easily.

watch
is in Vue 3, the new version of watch is compared with the old version of Vue 2, and the way of use has changed a lot!
Recall that Vue 2
is used in Vue 2 like this, and data and methods are configured at the same level:

export default {
    
    
  data() {
    
    
    return {
    
    
      // ...
    }
  },
  // 注意这里,放在 `data` 、 `methods` 同个级别
  watch: {
    
    
    // ...
  },
  methods: {
    
    
    // ...
  },
}

And there are many types, the types of option API are as follows:

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

Too many joint types means that the usage is complicated. The following is a good example. Although it comes from the usage introduction on the official website, the excessive usage also reflects that it is not very friendly to beginners. It may be confusing for the first time:

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

Of course, there will definitely be developers who think it is a good thing to choose this way, just choose the one that suits them, but the author still thinks that this way of writing is not so friendly to beginners, and it is a bit too complicated. If one usage can adapt to various Wouldn't it be better to have various scenes?

TIP

另外需要注意的是,不能使用箭头函数来定义 Watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue) )

因为箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向组件实例, this.updateAutocomplete 将是 undefined

Vue 2 can also listen to a certain data through the use of this.$watch() API, which accepts three parameters: source, callback and options.

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

Since the usage of this.$watch is similar to that of Vue 3, so I won’t review too much here, please read the Vue 3 section directly.

Understand
the combined API writing method of Vue 3 in Vue 3. watch is a function that can accept 3 parameters (retaining the usage of Vue 2's this.$watch), which is much simpler in terms of use.

import {
    
     watch } from 'vue'

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

The following content is based on the usage of Vue 3's combined API.
The TS type of the API
Before understanding the usage, let’s have a brief understanding of its TS type declaration. As a combined API, watch has two types of declarations according to the usage method: 1. TS type for basic usage, see the
basic usage section for details

// 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. The TS type of batch listening, see the batch listening section for details

// 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)[]
// ...

But whether it is basic usage or batch listening, you can see that this API accepts three input parameters:
insert image description here
and returns a function that can be used to stop listening (see: stop listening).
The data source to be listened to.
The TS type of the above API has a certain understanding of the composition of the watch API. Here, the type of data source and usage restrictions are explained first.

TIP

如果不提前了解,在使用的过程中可能会遇到 “侦听了但没有反应” 的情况出现。

另外,这部分内容会先围绕基础用法展开说明,批量侦听会在 批量侦听 部分单独说明。

The first parameter source of the watch API is the data source to be monitored, and its TS type is as follows:

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

You can see that the data that can be used for listening is a variable ( Ref ) defined through the responsive API, or a calculated data ( ComputedRef ), or a getter function ( () => T ).

Therefore, if the defined watch can perform the expected behavior, the data source must be responsive or a getter. If you just define a common variable through let and then change the value of this variable, it cannot be listened to.

TIP

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

The callback function after monitoring
The TS type of the above API introduces the composition of the watch API. Like the data source, first understand the definition of the callback function.

TIP

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

The second parameter callback of the watch API is the behavior to be performed when the data changes are detected, and its TS type is as follows:

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

At first glance it has three parameters, but in fact these parameters are not defined by itself, but passed by the watch API, so whether it works or not, they are all there: Note: the
insert image description here
first parameter is a new value, the second It is the original old value!

Like other JS functions, when using the callback function of watch, you can name these three parameters arbitrarily, for example, name value as newValue which is easier to understand.

TIP

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

In addition, by default, watch is lazy, that is, the callback is only executed when the monitored data source changes.

Basic usage
When you come here, you have a certain understanding of the two required parameters. Let’s take a look at the basic usage first, which is the most commonly written program in daily life. You only need to pay attention to the first two required parameters.

// 不要忘了导入要用的 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 })
      }
    )
 }
})

For general business scenarios, basic usage is sufficient.

If there are multiple data sources to listen to, and the behavior to be performed after listening to changes is the same, then batch listening can be used.

In special cases, some special usages can be done with the listening option, see the following section for details.
Batch listening
If there are multiple data sources to listen to, and the behavior to be executed after listening to the change is the same, the first reaction may be written like this:
1. Extract the same processing behavior as a public function
2. Then define multiple listener operation, passed to this public function

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

There is nothing wrong with writing this way, but in addition to the way of extracting common codes, the watch API also provides a usage of batch monitoring. The difference from the basic usage is that the data source and callback parameters are all in the form of an array.

Data source: It is passed in as an array, and each item in it is a responsive data.

Callback parameters: the original value and newValue have also become arrays, and the order in each array is consistent with the sorting of the data source array.

You can see the following example more intuitively:

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

Under what circumstances may batch listening be used? For example, a subcomponent has multiple props. When any prop changes, the initialization function needs to be executed to reset the state of the component. Then this function can be used at this time!

TIP

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

Listening options
As mentioned in the TS type of the API, the watch API also accepts the third parameter options, some optional listening options.
Its TS type is as follows:

// 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 is passed in the form of an object, and there are the following options:
insert image description here
The e of onTrack and onTrigger is a debugger event. It is recommended to place a debugger statement in the callback to debug dependencies. These two options only take effect in development mode.

TIP

deep 默认是 false
但是在侦听 reactive 对象或数组时,会默认为 true
详见 侦听选项之 deep。

The deep option of the listening option
accepts a Boolean value, which can be set to true to enable deep listening, or false to turn off deep listening. By default, this option is false to turn off deep listening, but there are exceptions.

When set to false, if you directly listen to a responsive reference type data (eg Object, Array ... ), although the value of its property changes, it is unchanged for itself, so it will not trigger The callback of the watch.

Here is an example with deep listening turned off:

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

In a situation like this, you need to set deep to true to trigger listening.

It can be seen that the above example uses the ref API specifically, because the object defined through the reactive API cannot successfully set deep to false (this is not explained in the current official website document, and finally found in the source code of the watch API Answer).

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

This situation is the "special case" mentioned above, and you can use the isReactive API to judge whether you need to manually enable deep listening.

// 导入 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
  },
})

The immediate of the listening option
has been learned in the callback function after listening. watch is lazy by default, that is, the callback is only executed when the monitored data source changes.

What does this sentence mean? Take a look at this code. In order to reduce the interference of the deep option, change the type to string data for demonstration. Please pay attention to the comments:

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

It can be seen that the listening callback is not triggered when the data is initialized. If necessary, it can be triggered directly through the immediate option.

The immediate option accepts a boolean value, which is false by default, and can be set to true to make the callback execute immediately.

Change it to this, please note the highlighted code parts and new comments:

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

Note that with the immediate option, the listener of the data source cannot be canceled at the first callback, see the stop listener section for details.

Flush of listener options The flush
option is used to control the call timing of the listener callback, and accepts the specified string. The optional values ​​are as follows, and the default is 'pre'.
insert image description here
For 'pre' and 'post', the callback is buffered using a queue. Callbacks are added to the queue only once.

Even if the observed value changes multiple times, the intermediate change of the value will be skipped and not passed to the callback, which not only improves performance, but also helps ensure data consistency.

For more information about flush, see When Callbacks Are Fired.

Stop listening
If you use watch in setup or script-setup, the component will also be stopped when it is uninstalled. Generally, you don't need to care about how to stop listening.

But sometimes you may want to cancel manually, and Vue 3 also provides a method.

TIP

随着组件被卸载一起停止的前提是,侦听器必须是 同步语句 创建的,
这种情况下侦听器会绑定在当前组件上。

如果放在 setTimeout 等 异步函数 里面创建,
则不会绑定到当前组件,因此组件卸载的时候不会一起停止该侦听器,
这种时候就需要手动停止侦听。

It is mentioned in the TS type of the API that when defining a watch behavior, it will return a function used to stop listening.

The TS type of this function is as follows:

export declare type WatchStopHandle = () => void

The usage is very simple, just do a simple understanding:

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

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

But one thing to note is that if the immediate option is enabled, it cannot be executed when the listener callback is triggered for the first time.

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

You will get an error message telling unwatch that this variable cannot be accessed before it is initialized:

Uncaught ReferenceError: Cannot access 'unwatch' before initialization

There are currently two options to achieve this:

Solution 1: Use var and judge the variable type, and use var variable promotion to achieve the goal.

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

However, var is already an outdated statement, and it is recommended to use let in Scheme 2.

Solution 2: Use let and judge the variable type.

// 如果不想用 any ,可以导入 TS 类型
import type {
    
     WatchStopHandle } from 'vue'

// 这里改成 let ,但是要另起一行,先定义,再赋值
let unwatch: WatchStopHandle
unwatch = watch(
  message,
  // 侦听回调
  () => {
    
    
    // 这里加一个判断,是函数才执行它
    if (typeof unwatch === 'function') {
    
    
      unwatch()
    }
  },
  // 侦听选项
  {
    
    
    immediate: true,
  }
)

Listening effect cleaning
In the callback function section after listening, a parameter onCleanup is mentioned, which can help register a cleaning function.

Sometimes the callback of the watch will perform asynchronous operations. When the watch detects data changes, these operations need to be canceled. This function is used for this, and the cleanup function will be called in the following situations:


The watcher is stopped when the watcher is about to rerun (the component is uninstalled or manually stopped listening)

TS type:

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

The usage is relatively simple, just pass in a callback function to run, but it should be noted that the cleanup behavior needs to be registered before stopping the listening, otherwise it will not take effect.

Continue to add code based on the last immediate example in Stop Listening, please pay attention to the timing of registration:

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

watchEffect
If a function contains multiple data that needs to be monitored, it is too troublesome to listen to each data one by one. In Vue 3, you can directly use the watchEffect API to simplify the operation.

TS type of API

The types of this API are as follows. When using it, you need to pass in a side effect function (equivalent to the callback function after watch's listening), and you can also pass in some optional listening options according to the actual situation.

Like the watch API, it also returns a function to stop listening.

// watchEffect 部分的 TS 类型
// ...
export declare type WatchEffect = (onCleanup: OnCleanup) => void

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

The side effect function will also pass in a cleanup callback as a parameter, which is the same usage as watch's listening effect cleanup.

It can be understood as a simplified version of watch, where is the specific simplification? See usage example below.

usage example

It immediately executes a function passed in, while reactively tracking its dependencies and re-running the function when its dependencies change.

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

Differences from watch
Although watchEffect is theoretically a simplified operation of watch and can be used to replace batch monitoring, they also have certain differences:
1. Watch can access the value before and after the listening state changes, but watchEffect does not.

2. watch is executed when the property changes, while watchEffect will execute once by default, and then it will also be executed when the property changes.

The meaning of the second point can be more intuitively understood by looking at the following code:

Use 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)
  },
})

Use 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)
  },
})

Available listening options
Although the usage is similar to watch, some options are also simplified. Its listening options TS type is as follows:

// 只支持 base 类型
export declare interface WatchOptionsBase extends DebuggerOptions {
    
    
  flush?: 'pre' | 'post' | 'sync'
}
// ...

// 继承的 debugger 选项类型
export declare interface DebuggerOptions {
    
    
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
}
// ...

Comparing with watch API, it doesn't support deep and immediate, please keep this in mind, other usage is the same.

For the use of the flush option, see the flush option of the listener option, and for onTrack and onTrigger, see the listener option section for details.

watchPostEffect
The alias of the watchEffect API when using the flush: 'post' option. For details, see the flush section of the listener option.

TIP

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

watchSyncEffect
The alias for the watchEffect API when using the flush: 'sync' option. For details, see the flush section of the listen option.

TIP

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

Guess you like

Origin blog.csdn.net/m0_49515138/article/details/128250061