跳至主要內容

watch 侦听器

wzCoding大约 3 分钟VueVue3watch 侦听器

watch 侦听器

watch 侦听器是 Vue3 中的一个重要特性,它可以监测数据的变化,并在它们发生变化时调用传入的回调函数来帮助我们完成一些其他操作。

基本用法

watch 侦听器接收 3 个参数:

  • 第一个参数是我们要侦听的数据(响应式数据对象、getter 函数、前面两种数据组成的数组)
  • 第二个参数是回调函数,当数据发生变化时,回调函数会被调用
  • 第三个参数是可选的:
    • immediate:一个布尔值,表示是否在侦听开始之后立即调用回调函数
    • deep:一个布尔值,表示是否深度侦听,即是否在侦听数据对象时,递归地侦听其所有属性
    • once:一个布尔值,表示是否只调用一次回调函数,即在侦听开始之后立即调用回调函数,然后停止侦听
    • flush:一个字符串,表示回调函数的调用时机,可以是 pre(表示在 DOM 更新之前调用)、post(表示在 DOM 更新之后调用)、sync(表示同步调用)

监听响应式数据对象

import { watch } from 'vue';
const obj = ref({ count: 0 });

watch(obj, (newValue, oldValue) => {
  console.log('obj changed', 'newValue ==>', newValue, 'oldValue ==>', oldValue);
}, {
  immediate: true,
  deep: true
});

obj.value.count = 1;
// 输出:
// obj changed
// newValue ==> { count: 1 }
// oldValue ==> { count: 0 }

监听 getter 函数

import { watch } from 'vue';
const obj = ref({ count: 0 });

watch(() => obj.value.count, (newValue, oldValue) => {
  console.log('obj.count changed', 'newValue ==>', newValue, 'oldValue ==>', oldValue);
}, {
    immediate: true,
    deep: true
});

obj.value.count = 1;
// 输出:
// obj.count changed
// newValue ==> 1
// oldValue ==> 0

监听多个数据

import { watch } from 'vue';
const obj = ref({ count: 0 });
const num = ref(0);

watch([() => obj.value.count, num], ([newValue1, newValue2], [oldValue1, oldValue2]) => {
  console.log('obj.count or num changed', 'newValue1 ==>', newValue1, 'newValue2 ==>', newValue2, 'oldValue1 ==>', oldValue1, 'oldValue2 ==>', oldValue2);
}, {
    immediate: true,
    deep: true
});

obj.value.count = 1;
num.value = 1;
// 输出:
// obj.count or num changed
// newValue1 ==> 1
// newValue2 ==> 1
// oldValue1 ==> 0
// oldValue2 ==> 0

实现原理

watch 侦听器的实现原理是利用了 Vue 的 响应式系统。在响应式系统中,通过 effect 方法来执行传入的回调函数。

watch 也是懒执行的,即只有在侦听开始之后,才会执行传入的回调函数。它同样有着自身内部的 effect 方法,用于控制回调函数的执行。

watch 的实现还依赖 traverse 函数,它用来读取 watch 所监听的源对象。

以下是简单的代码实现:

function watch(source, callback, options = {}) {
    //定义 getter,用来读取监听数据的值
    let getter

    //参数归一化处理,将传入的 source 转换为 getter 函数
    if (typeof source === 'function') {
        getter = source
    }
    else {
        getter = () => traverse(source)
    }

    //定义 watch 监听的新值和旧值
    let oldValue, newValue

    //定义清理过期回调的函数
    let cleanup

    //将用户传入的过期回调函数存储在 cleanup 中
    function onInvalidate(fn) {
        cleanup = fn
    }
    
    //定义 job 任务函数
    const job = () => {

        //获取新值
        newValue = effectFn()
        
        //如果 cleanup 清理过期回调的函数存在,就先清理过期的回调,然后再执行
        if (cleanup) {
            cleanup()
        }

        //执行传入的 callback
        callback(newValue, oldValue, onInvalidate)
        
        //获取旧值
        oldValue = newValue
    }

    //注册 effectFn 副作用函数
    const effectFn = effect(

        //执行 getter 读取数据
        () => getter(),
        {   
            //设置 lazy 选项,表示懒执行
            lazy: true,

            //设置 scheduler 调度器函数
            scheduler: () => {

                //判断 options 中的 flush 是否为 post
                if (options.flush === 'post') {

                    //如果 flush 为 post,则将 job 函数放到微任务队列中执行
                    const p = Promise.resolve()
                    p.then(job)
                }
                //否则直接执行 job 函数
                else {
                    job()
                }
            }
        }
    )
    
    //判断 options 中的 immediate 选项是否为 true
    if (options.immediate) {

        //是则直接执行 job 函数
        job()
    }

    //否则手动执行副作用函数,获取旧值
    else {
        oldValue = effectFn()
    }
}

function traverse(value, seen = new Set()){
   //如果被读取的数据是原始值或者已经被读取过,就直接返回
   if(typeof value !== 'object' || value === null || seen.has(value)) return
   //否则将数据添加到 seen 这个集合中用来缓存
   seen.add(value)
   
   //循环遍历传入的对象数据的每个属性,递归的调用 traverse 函数处理
   //(这里暂时不考虑数组的情况,数组直接使用循环即可)
   for(const key in value){
      traverse(value[key], seen)
   }

   return value
}