2025-09-27 05:05:12

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 需要的是函数,而 watchsource 不一定是函数。

所以第一步要做的,就是把这些不同形式的参数统一起来。

参数归一化

先写一个 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 无论接收到的是 refreactive 还是 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
}