watch 常见的使用方式有:
const count = ref(0)
// 单个 ref
watch(count, (newValue, oldValue) => {})
// getter 函数
watch(() => count.value, (newValue, oldValue) => {})
// 多个来源
watch([count], ([newValue]) => {})
const state = reactive({ count: 0 })
// reactive 对象
watch(state, () => {})
先写一个最基础的测试:
describe('watch', () => {
it('base watch', () => {
const count = ref(0)
let newV
let oldV
watch(
() => count.value,
(newValue, oldValue) => {
newV = newValue
oldV = oldValue
}
)
count.value = 123
expect(newV).toBe(123)
expect(oldV).toBe(0)
})
})
watch 支持的输入形式很多,但底层最终都要交给 ReactiveEffect 处理。问题就在于:ReactiveEffect 需要的是函数,而 watch 的 source 不一定是函数。
所以第一步要做的,就是把这些不同形式的参数统一起来。
参数归一化
先写一个 normalizeSource,把不同类型的 source 都转换成函数:
function normalizeSource(source) {
if (isRef(source)) {
return () => source.value
}
else if (isReactive(source)) {
return () => source
}
else if (isFunction(source)) {
return source
}
return () => {}
}
这样 watch 无论接收到的是 ref、reactive 还是 getter,最后都可以统一交给 ReactiveEffect。
这里可以先简化一下实现:创建一个 WatcherEffect,把归一化后的 getter 传给它,再通过 scheduler 自定义触发后的行为。
export function watch(source, cb, options = {}) {
const effect = new WatcherEffect(source, cb, options)
}
class WatcherEffect extends ReactiveEffect {
oldValue: any
cb: Function
constructor(source, cb, options) {
const getter = normalizeSource(source)
super(getter)
this.cb = cb
this.scheduler = () => {
this.job()
}
}
job() {
const newValue = super.run()
const oldValue = this.oldValue
this.cb(newValue, oldValue)
this.oldValue = newValue
}
}
接下来,在初始化阶段主动执行一次 run,就能完成依赖收集,并保存初始值:
export function watch(source, cb, options) {
const effect = new WatcherEffect(source, cb, options)
effect.oldValue = effect.run()
}
到这里,watch 的基础能力就已经具备了。
Options 参数
watch 里比较常见的几个选项有:
immediate:初始化时立即执行一次deep:深度监听once:只执行一次,之后停止监听
先处理 immediate。如果传了这个选项,就在创建完成后立刻执行一次 job;
export function watch(source: any, cb: Function, options: WatchOptions = {}) {
const { immediate } = options
const effect = new WatcherEffect(source, cb, options)
if (immediate) {
effect.job()
}
else {
effect.oldValue = effect.run()
}
}
停止监听
在处理 once 之前,得先让副作用具备“停止”的能力
所以要先在 ReactiveEffect 上补上 stop:
// effect.ts
export class ReactiveEffect implements ReactiveNode {
deps: Link | undefined
depsTail: Link | undefined
tracking = false
active = true
constructor(public fn: Function) {}
run() {
if (!this.active) {
return this.fn()
}
// ...
}
stop() {
if (this.active) {
startTrack(this)
endTrack(this)
this.active = false
}
}
// ...
}
处理 once
有了 stop 之后,在构造函数里包装一层回调:执行完用户传入的 cb 之后,立刻调用 this.stop()。
constructor(source, cb, options: WatchOptions) {
const { once } = options
if (once && cb) {
const _cb = cb
cb = (...args) => {
_cb(...args)
this.stop()
}
}
}
处理 deep
deep 稍微特殊一点。
如果 watch 的是 reactive 对象,那么在 Vue 里通常会默认按深度监听来处理。到了 Vue 3.5,deep 还支持传数字,用来控制遍历层级。
所以在归一化 source 时,可以先把 reactive 的默认 deep 补上:
function normalizeSource(source, options: WatchOptions) {
if (isRef(source)) {
return () => source.value
}
else if (isReactive(source)) {
options.deep ??= true
return () => source
}
else if (isFunction(source)) {
return source
}
return () => {}
}
如果传入的是 reactive,并且用户没有显式指定 deep,那就把它设成 true。 再根据 deep 的值包装一层 getter:
true:遍历到最深层- 数字:遍历到指定层级
最终完整代码如下:
import { hasChanged, isFunction, isObject } from '@vue/shared'
import { ReactiveEffect } from './effect'
import { isReactive } from './reactive'
import { isRef } from './ref'
export interface WatchOptions {
immediate?: boolean
deep?: boolean | number
once?: boolean
}
export function watch(source: any, cb: Function, options: WatchOptions = {}) {
const { immediate } = options
const effect = new WatcherEffect(source, cb, options)
if (immediate) {
effect.job()
}
else {
effect.oldValue = effect.run()
}
return () => effect.stop()
}
class WatcherEffect extends ReactiveEffect {
oldValue: any
cb: Function
cleanupFn: (() => void) | null = null
deep: boolean | number
constructor(source, cb, options: WatchOptions) {
const { once } = options
let getter = normalizeSource(source, options)
const deep = options.deep
if (deep) {
const baseGetter = getter
const depth = deep === true ? Infinity : deep
getter = () => traverse(baseGetter(), depth)
}
super(getter)
this.deep = deep
if (once && cb) {
const _cb = cb
cb = (...args) => {
_cb(...args)
this.stop()
}
}
this.cb = cb
this.scheduler = () => {
this.job()
}
}
onCleanup = (fn: () => void) => {
this.cleanupFn = fn
}
job() {
if (this.cleanupFn) {
this.cleanupFn()
this.cleanupFn = null
}
const newValue = super.run()
const oldValue = this.oldValue
if (this.deep || hasChanged(newValue, oldValue)) {
this.cb?.(newValue, oldValue, this.onCleanup)
}
this.oldValue = newValue
}
}
function normalizeSource(source, options: WatchOptions) {
if (isRef(source)) {
return () => source.value
}
else if (isReactive(source)) {
options.deep ??= true
return () => source
}
else if (isFunction(source)) {
return source
}
return () => {}
}
function traverse(value: any, depth: number = Infinity, seen = new Set<any>()) {
if (!isObject(value) || depth <= 0) {
return value
}
if (seen.has(value)) {
return value
}
depth--
seen.add(value)
for (const key in value) {
traverse(value[key], depth, seen)
}
return value
}