侦听器:侦听器的实现原理和使用场景是什么?(上)

94 阅读7分钟

侦听器:侦听器的实现原理和使用场景是什么?(上)

watch API 的用法

1. watch API 可以侦听一个 getter 函数,但是它必须返回一个响应式对象,当该响应式对象更新后,会执行对应的回调函数。

import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => {
    // 当 state.count 更新,会触发此回调函数
})

2. watch API 也可以直接侦听一个响应式对象,当响应式对象更新后,会执行对应的回调函数。

import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (count, prevCount) => {
    // 当 count.value 更新,会触发此回调函数
})

3. watch AP 还可以侦听多个响应式对象,任意一个响应式对象更新后,就会执行对应的回调函数。

import { ref, watch } from 'vue'
const count = ref(0)
const count2 = ref(1)
watch([count, count2],([count, count2], [prevCount, prevCount2]) => {
    // 当 count.value 或者 count2.value 更新,会触发此回调函数
})

watch API 实现原理

/*
    watch 函数内部调用了 doWatch 函数,调用前会在非生产环境下判断第二个参数 cb 是不是一个函数,如果不是则会报警以告诉用户应该使用 watchEffect(fn, options) API,watchEffect API。
*/
function watch(source, cb, options) {
    if ((process.env.NODE_ENV !== 'production') && !isFunction(cb)) {
        warn(`...`)
    }
    return doWatch(source, cb, options)
}
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
    // 标准化 source
    // 构造 applyCb 回调函数
    // 创建 scheduler 时序执行函数
    // 创建 effect 副作用函数
    // 返回侦听器销毁函数
}

标准化 source

标准化 source 流程如下:
// source 不合法的时候会报警告
const warnInvalidSource = (s) => {
    warn(...)
}
// 当前组件实例
const instance = currentInstance
let getter
if (isArray(source)) {
    getter = () => source.map(s => {
            if (!isRef(s)) {
                return s.value
            } else if (isReactivce(s)) {
                return traverse(s)
            } else if (isFunction(s)) {
                return callWithErrorHandling(s, instance, 2 /* WATCH_GETTER */)
            } else {
                (process.env.NODE_ENV !== 'production') && warnInvalidSource(s)
            }
        })
} else if (isRef(source)) {
    getter = () => source.value
} else if (isReactive(source)) {
    getter = () => source
    deep = true
} else if (isFunction(source)) {
    if (cb) {
        // getter with cb
        getter = () => callWithErrorhandling(source, instance, 2 /* WATCH_GETTER */ )
    } else {
    // watchEffect 的逻辑
}
} else {
    getter = NOOP
    (process.env.NODE_ENV !== 'production') && warnInvalidSource(source)
}
if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
}
其实,source 标准化主要是根据 source 的类型,将其变成 getter 函数。具体来说
    1.如果 source 是 ref 对象,则创建一个访问 source.value 的 getter 函数;
    2.如果 source 是 reactive 对象,则创建一个访问 source 的 getter 函数,并设置 deep 为 true;
    3.如果 source 是一个函数,则会进一步判断第二个参数 cb 是否存在,对于 watch API 来说,cb 是一定存在且是一个回调函数,这种情况下,getter 就是一个简单的对 source 函数封装的函数。
如果 source 不满足上述条件,则在非生产环境下报警告,提示 source 类型不合法。

构造回调函数

处理完 watch API 第一个参数 source 后,接下来处理第二个参数 cb。
cb 是一个回调函数,它有三个参数:第一个 newValue 代表新值;第二个 oldValue 代表旧值。第三个参数 onInvalidate。
其实这个的 API 设计非常好理解,即侦听一个值的变化,如果值变了就执行回调函数,回调函数里可以访问到新值和旧值。
构造回调函数逻辑如下:
    let cleanup
    // 注册无效回调函数
    const onInvalidate = (fn) => {
        cleanup = runner.options.onStop = () => {
            callWithErrorHandling(fn, instance, 4 /* WATCH_CLEANUP */)
        }
    }
    // 旧值初始化
    let odlValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE /* [] */
    // 回调函数
    const applyCb = cb ? () => {
        // 组件销毁,则直接返回
        if (instance && instance.isUnmounted) {
            return
        }
        // 求得新值
        const newValue = runner()
        if (deep || hasChanged(newValue, oldValue)) {
            // 执行清理函数
            if (cleanup) {
                cleanup()
            }
            callWithAsyncErrorHandling(cb, instance, 3 /*WATCH_CALLBACK */, [
                newValue,
                // 第一次更改时传递旧值为 undefined
                oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
                onInvalidate
            ])
            // 更新旧值
            oldValue = newValue
        }
    } : viod 0
首先,watch API 和组件实例相关,因为通常我们会在组件的 setup 函数中使用它,当组件销毁后,回调函数 cb 不应该被执行而是直接返回。
接着,执行 runner 求得新值,这里实际上就是执行前面创建的 getter 函数求新值。
最后进行判断,如果是 deep 的情况或者新旧值发生了变化,则执行回调函数 cb,传入参数 newValue 和 oldValue。注意,第一次执行的时候旧值的初始值是空数组或者 undefined。执行完回调函数 cb 后,把旧值 oldValue 再更新为 newValue,这是为了下一次比对。

创建 sheduler

sheduler 的作用是根据某种调度的方式去执行某种函数,在 watch API 中,主要影响到的是回调函数的执行方式。我们来看一下它的实现逻辑:
    const invoke = (fn) => fn()
    let scheduler
    if (flush === 'sync') {
        // 同步
        scheduler = invoke
    } else if(flush === 'pre') {
        scheduler = job => {
            if (!instance || instance.isMounted) {
                // 进入异步队列,组件更新前执行
                queueJob(job)
            ] else {
                // 如果组件还没挂载,则同步执行确保在组件挂载前
                job()
            }
        }
    } else {
        // 进入异步队列,组件更新后执行
        sheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
    }
Watch API 的参数除了source 和 cb,还支持第三个参数 options,不同的配置决定了 watcher 的不同行为。前面我们也分析了 deep 为 true 的情况,除了 source 为 reactive 对象时会默认把 deep 设置为 true,你也可以主动传入第三个参数,把 deep 设置为 true。
这里,scheduler 的创建逻辑受到了第三个参数 Options 肿的 flush 属性值的影响,不同的 flush 决定了 watcher 的执行时机。
    当 flush 为 sync 的时候,表示它是一个同步 watcher,即当数据变化时同步执行回调函数。
    当 flush 为 pre 的时候,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行。
    如果没设置 flush,那么回调函数通过queuePostRenderEffect 的方式在组件更新之后执行。
watcher 的回调函数是通过一定的调度方式执行的。

创建 effect

下面是 watcher 内部创建的 effect 函数的逻辑代码:
    const runner = effect(getter, {
        // 延时执行
        lazy: true,
        // computed effect 可以优先于普通的 effect 先运行,比如组件渲染的 effect
        computed: true.
        onTrack,
        onTrigger,
        scheduler: applyCb ? () => scheduler(applyCb) : scheduler
    })
    // 在组件实例中记录这个 effect
    recordInsanceBoundEffect(runner)
    // 初次执行
    if (applyCb) {
        if (immediate) {
            applyCb()
        } else {
            // 求旧值
            oldValue = runner()
        }
    } else {
        // 没有 cb 的情况
        runner()
    }
这块代码逻辑是整个 watcher 实现的核心部分,即通过 effect API 创建一个副作用函数 runner,我们需要关注以下几点。
    runner 是一个 computed effect。因为 computed effect 可以优先于普通的 effect(比如组件渲染的 effect)先运行,这样就可以实现当配置 flush 为 pre 的时候,watcher 的执行可以优先于组件更新。
    runner 执行的方式。runner 是 lazy 的,它不会在创建后立刻执行。第一次手动执行 runner 会执行前面的 getter函数,访问响应式数据并做依赖收集。注意,此时 activeEffect 就是 runner,这样在后面更新响应式数据时,就可以触发 runner 执行 scheduler 函数,以一种调度方式来执行回调函数。
    runner 的返回结果。手动执行 runner 就相当于执行了前面标准化的 getter 函数,getter 函数的返回值就是 watcher 计算出的值,所以我们第一次执行 runner 求得的值可以作为 oldValue。
    配置了 immediate 的情况。当我们配置了 immediate,创建完 watcher 会立刻执行 applyCb 函数,此时 oldValue 还是初始值,在 applyCb 执行时也会执行 runner 进而执行前面的 getter 函数做依赖收集,求得新值。

返回销毁函数

最后,会返回侦听器销毁函数,也就是 watch API 执行后返回的函数。
    return () => {
        stop(runner)
        if (instance) {
            // 移除组件 effects 对这个 runner 的引用
            remove(instance.effects, runner)
        }
    }
    function stop(effect) {
        if (effect.active) {
            cleanup(effect)
            if (effect.options.onStop) {
                effect.options.onStop()
            }
            effect.active = false
        }
    }
销毁函数内部会执行 stop 方法让 runner 失活,并清理 runner 的相关依赖,这样就可以停止对数据的侦听。并且,如果是在组件中注册的 watcher,也会移除组件 effects 对这个 runner 的引用。
总结一下就是:侦听器的内部设计很巧妙,我们可以侦听响应式数据的变化,内部创建 effect runner,首次执行 runner 做依赖收集,然后再数据发生变化后,以某种调度方式去执行回调函数。