监听类方法

监听(侦听)类方法的作用类似于配置式 API 中使用的 watch 方法、computed 方法等。监听类方法主要的使用场景是提供对于响应式数据改变的追踪和影响,并提供一些钩子函数。本节主要介绍组合式 API 中的 computed 方法、watchEffect 方法和 watch 方法。

computed 方法

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 getset 函数的对象来创建一个可写的 ref 对象。

在配置式 API 中,computed 是指计算属性,在计算属性中可以完成各种复杂的逻辑,包括运算、函数调用等,只要最终返回一个结果就可以。计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。组合式 API 中的 computed 也是类似的,使用方法如示例代码 4-4-1 所示。

示例代码4-4-1 computed方法
<div id="app">
    {{info}}
</div>

Vue.createApp({
    setup() {
        const state = Vue.reactive({
            name: "John",
            age: 18
        });
        const info = Vue.computed(() => { // 创建一个计算属性,依赖 name 和 age
            console.log('computed')
            return state.name + ',' + state.age
        });

        info.value = 1  // 抛出警告 Write operation failed: computed value is readonly

        setTimeout(() => {
            state.age = 20  // info 动态修改
        }, 1000)

        setTimeout(() => {
            state.age = 20  // 取上一次修改后的数据,即缓存的数据
        }, 2000)

        return {
            info
        }
    }
}).mount("#app")

上面的代码中,计算属性 info 依赖 state 中的 agename,当它们发生变化时,会导致 info 变化,同时如果每次变化的值相同,则取上次修改后的缓存数据,不会再次执行 computed 中的方法,这和配置式 API 中的 computed 是一致的。同时,info 也是一个不可变的响应式对象,尝试修改会抛出警告。

computed 方法也可以接收一个对象,分别配置 getset 方法,这样分别设置读对应 get 方法和写对应调用 set 方法的返回值,代码如下:

const info = Vue.computed({
    get: () => state.name + ',' + state.age,
    set: val => {
        state.age = val - 1
    }
});
info.value = 21

watchEffect/watchPostEffect/watchSyncEffect 方法

watchEffect 方法可以监听响应式对象的改变,参数是一个函数,这个函数中所依赖的响应式对象如果发生变化,都会触发这个函数,如示例代码4-4-2所示。

  • 第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。

  • 第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。

    默认情况下,侦听器将在组件渲染之前执行(flush: 'pre')。设置 flush: 'post' 将会使侦听器延迟到组件渲染之后再执行。详见回调的触发时机。在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: 'sync' 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。

  • 返回值是一个用来停止该副作用的函数。

示例代码4-4-2 watchEffect 方法
<div id="app">
    {{info}}
</div>
Vue.createApp({
    setup() {
        const state = Vue.reactive({
            name: "John",
            age: 18
        });
        const count = Vue.ref(0)
        const countNo = Vue.ref(0)
        const info = Vue.computed(() => { // 创建一个计算属性,依赖name和age
            return state.name + ',' + state.age
        });
        Vue.watchEffect(()=>{
            console.log('watchEffect')
            console.log(info.value) // 依赖了info
            console.log(count.value) // 依赖了count
        })
        setTimeout(()=>{
            state.age = 20 // 触发watchEffect
        },1000)
        setTimeout(()=>{
            count.value = 3 // 触发watchEffect
        },2000)
        setTimeout(()=>{
            countNo.value = 5 // 不触发watchEffect
        },3000)
        return {info}
    }
}).mount("#app")

watchEffect 在组件的 setup 方法或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止,在一些情况下,也可以显式地调用返回值以停止侦听,代码如下:

const stop = watchEffect(() => {
/* ... */
})
...
stop()
  • watchPostEffect()watchEffect() 使用 flush: 'post' 选项时的别名。

  • watchSyncEffect()watchEffect() 使用 flush: 'sync' 选项时的别名。

watch 方法

在配置式 API 中,watch 是指监听器,组合式 API 中同样提供了 watch 方法,其使用场景和用法是一致的,主要是对响应式对象变化的监听。其和 watchEffect 相比有些类似,主要区别是:

  • watch 需要监听特定的数据源,并执行对应的回调函数,而 watchEffect 不需要指定监听属性,它会自动收集依赖,只要回调函数中使用了响应式的属性,那么当这些属性变更的时候,这个回调都会执行。

  • watch 在默认情况下,watch 是惰性的,即只有当被监听的源发生变化时才执行回调。

  • watch 可以访问监听状态变化前后的新旧值。

watch 监听单个数据源,第一个参数可以是返回值的 getter 函数,也可以是一个响应式对象,第二个参数是触发变化的回调函数,如示例代码 4-4-3 所示。

示例代码4-4-3 watch 监听单个数据源
Vue.createApp({
    setup() {
        // 侦听一个 getter
        const state = Vue.reactive({ count: 0 })
        Vue.watch(() => state.count,
            (count, prevCount) => {
                console.log(count, prevCount)
            }
        )
        // 直接侦听ref
        const count = Vue.ref(0)
        Vue.watch(count, (count, prevCount) => {
            console.log(count, prevCount)
        })
        setTimeout(()=>{
            state.count = 1
            count.value = 2
        })
        return {}
    }
}).mount("#app")

watch 监听多个数据源,第一个参数是多个响应式对象的数组,第二个参数是触发变化的回调函数,如示例代码 4-4-4 所示。

示例代码4-4-4 watch 监听多个响应式对象
Vue.createApp({
    setup() {
        const state = Vue.reactive({name: 'John'})
        const count = Vue.ref(0)

        Vue.watch([count, state], ([newCount, newState], [prevCount, prevState]) => {
            console.log(newCount, prevCount)
            // [2, {name:"Ted"}] [0, {name:"John"}]
            console.log(newState, prevState)
        })

        setTimeout(() => {
            state.name = 'Ted'
            count.value = 2
        })

        return {}
    }
}).mount("#app")

watch 监听复杂响应式对象时,如果要完全深度监听,则需要添加 deep:true 配置,同时第一个参数需要为一个 getter 方法,并采用深度复制,如示例代码 4-4-5 所示。

Vue.createApp({
    setup() {
        const state = Vue.reactive({
            name: "John",
            age: 18,
            attributes: {
                attr: 'efg',
            }
        });
        Vue.watch(()=>JSON.parse(JSON.stringify(state)),// 利用深度复制
            (currentState, prevState) => {
                console.log(currentState.attributes.attr)// abc
                console.log(prevState.attributes.attr)// efg
            },{ deep: true })
        setTimeout(()=>{
            state.attributes.attr = 'abc'
        },1000)
        return {}
    }
}).mount("#app")

需要注意,深度监听需要对原始 state 进行深度复制并返回,可以采用 JSON.parse()JSON.stringify() 等方法进行复制,也可以采用一些第三方库,例如 lodash.cloneDeep 方法。

watch(source, callback, options);
  • source:需要被观察的数据,通常是一个响应式对象、ref 或一个 getter(如计算属性)。

  • callback:当 source 发生变化时调用的回调函数。该函数会接收两个参数:newValueoldValue,分别是新值和旧值。

  • options(可选):配置选项,用来控制 watch 的行为。

watch 提供了一个可选的 options 参数,用来控制一些细节行为。常见的选项包括:

  • immediate:是否在侦听开始后立即调用回调函数。默认值是 false,即只有在源数据变化时才会调用回调。如果设置为 true,则在监听开始时会立即调用回调。

    watch(count, (newCount) => {
      console.log('Immediate:', newCount);
    }, { immediate: true });
  • deep:是否深度观察对象内部的变化。适用于 reactive 对象。当你想要监听对象内部的某个属性时,设置 deep: true 会递归地观察该对象。

    watch(state, (newState) => {
      console.log('State changed:', newState);
    }, { deep: true });
  • flush:控制回调函数何时执行。可以设置为以下三种值:

    • sync:回调会在同一个同步队列中立即执行。

    • pre:回调会在组件更新之前执行。

    • post:回调会在组件更新之后执行(默认值)。

    watch(count, (newCount) => {
      console.log('Count updated:', newCount);
    }, { flush: 'post' });

监听计算属性

watch 也可以用来监听计算属性(computed)。与监听普通响应式数据类似,计算属性变化时会触发 watch 回调。

import { ref, computed, watch } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const doubled = computed(() => count.value * 2);

    watch(doubled, (newVal, oldVal) => {
      console.log(`Doubled value changed from ${oldVal} to ${newVal}`);
    });

    return { count, doubled };
  }
};

在这个示例中,watch 用来监听计算属性 doubled 的变化,当 count 改变时,doubled 也会变化,进而触发 watch 回调。

清除 watcher

watch 函数的回调是由 Vue 内部创建的一个副作用函数。如果需要在某些条件下停止观察(例如组件销毁或某个值改变),可以通过返回的停止函数来手动清除 watcher

import { ref, watch } from 'vue';

export default {
  setup() {
    const count = ref(0);

    const stop = watch(count, (newCount) => {
      console.log('Count changed:', newCount);
    });

    // 停止监听
    stop();

    return { count };
  }
};

在这个例子中,调用 stop() 可以停止对 count 的监听。