2025.09.21

watch:核心实现

watch 允许在 响应式数据发生变化时,执行特定的副作用(Side Effect)。核心思想是基于 effect 的 调度机制( scheduler)。

watch 允许在 响应式数据发生变化时,执行特定的副作用(Side Effect)。核心思想是基于 effect调度机制scheduler)。

⚙️ watch 参数回顾

参数描述
source监听的数据源, 可以是 ref reactive对象,或是一个返回监听值的 getter 函数
cb回调函数, 接受(newValue, oldValue)作为参数
options配置项,如(immediate(立即执行)、dep(深度监听), once(只执行一次))

🛠️ 基础实现

测试代码:


<script type="module">
  import {ref, watch} from '../dist/reactivity.esm.js'

  const count = ref(0)

  watch(count, (newV, oldV) => {
    console.log('--- Watch Fired ---')
    console.log('newV:', newV, 'oldV:', oldV)
  })

  // 模拟响应式数据更新
  setTimeout(() => {
    count.value = 1
    // 理想输出: newV: 1, oldV: 0
  }, 100)

</script>
  1. 实现 watch 时, 直接使用 reactiveEffect 类,而不是effect函数

effect 函数返回的是一个 runner。要获取 effect 内部 fn 的返回值(即监听数据的当前值),我们必须通过 ReactiveEffect 实例调用 effect.run()

// 原始的 effect 实现示例
export function effect(fn, options = {}) {
  const e = new ReactiveEffect(fn)
  Object.assign(e, options)
  e.run() // 首次执行并收集依赖

  const runner = e.run.bind(e)
  runner.effect = e

  return runner // runner 本身不返回 fn 的值
}
  1. 包装 sourcegetter 函数 ReactiveEffect 构造函数需要传入一个函数 (getter)。因为 source 可能是一个 ref 对象(而不是函数),我们需要对其进行统一的包装。
    import type { RefImpl } from './ref'
    import { EMPTY_OBJ } from '@vue/shared'
    import { ReactiveEffect } from './effect'
    import { isRef } from './ref'
    
    // --- 类型定义 ---
    export interface WatchOptions {
      immediate?: boolean
      deep?: boolean | number
      once?: boolean
    }
    
    // -----------------
    
    export function watch(
      source,
      cb,
      options: WatchOptions = EMPTY_OBJ
    ) {
      let getter: () => any
    
      // 1. 统一 source 为 getter 函数
      if (isRef(source)) {
        // 如果是 ref,getter 就是返回其 .value 的函数
        getter = () => source.value
      }
      else {
        // 假设是函数(针对 reactive 或 computed,此处简化)
        getter = source as () => any
      }
    
      // 2. 创建 ReactiveEffect 实例
      const effect = new ReactiveEffect(getter)
    }
    
  2. ⚙️ 核心:实现 job 调度器

job 函数是作为 effect.scheduler 传入的,它定义了当依赖数据更新时需要执行的操作流程。

步骤目的代码实现
1. 获取新值调用 effect.run() 获取最新的值。重新收集依赖const newValue = effect.run()
2. 执行回调调用提供的 cb, 传入 newValue 和之前保存的 oldValuecb(newValue,oldValue)
3. 更新旧值将当前的 newValue 保存为下一次变化的 oldValueoldValue = newValue
import { ReactiveEffect } from './effect'

// ... (其他导入)

export function watch(
  source: WatchSource,
  cb,
  options: WatchOptions = EMPTY_OBJ
) {
  let getter: () => any
  let oldValue: any // 用于存储上一次的值

  if (isRef(source)) {
    getter = () => source.value
  }
  else {
    getter = source as () => any
  }

  // --- 定义核心调度器 ---
  function job() {
    // 1. 运行 effect 拿到 newValue (同时重新收集依赖)
    const newValue = effect.run()

    // 2. 执行用户的回调
    cb(newValue, oldValue)

    // 3. 更新旧值,为下一次变化做准备
    oldValue = newValue
  }

  // -----------------------

  // 1. 创建 effect 实例
  const effect = new ReactiveEffect(getter)

  // 2. 设置调度器:当依赖变化时,执行 job 而不是 effect.run()
  effect.scheduler = job

  // 3. 首次执行:收集依赖,并获取初始的 oldValue
  // effect.run() 的返回值就是 getter 的返回值
  oldValue = effect.run()

  return () => {

  }
}

🛑 停止监听 (stop)

为了实现停止监听的功能,核心在于增强 ReactiveEffect 类,赋予它管理依赖和控制激活状态的能力。当调用 stop() 时,需要清除当前 effect 收集到的所有依赖,并将其标记为非激活状态。


<script type="module">
  import {ref, watch} from '../dist/reactivity.esm.js'

  const count = ref(0)

  // 接收 stop 函数
  const stop = watch(count, (newV, oldV) => {
    console.log('--- Watch Fired ---')
    console.log('newV:', newV, 'oldV:', oldV)
  })

  setTimeout(() => {
    count.value = 1 // 第一次触发: Fired (newV: 1, oldV: 0)
    setTimeout(() => {
      stop() // 停止监听!
      count.value = 2 // 第二次更新:不会触发 watch 回调
    }, 100)
  }, 100)
</script>
  1. 增强 ReactiveEffect 类 (支持 activestop)
    export class ReactiveEffect implements ReactiveNode {
      // ... (deps, depsTail, tracking, dirty 属性省略)
    
      // 🔴 核心属性:用于控制是否激活
      active = true
    
      constructor(public fn: Function) { /* ... */
      }
    
      // ... (run, notify, scheduler 方法省略)
    
      /**
       * 🛑 停止依赖追踪和响应
       */
      stop() {
        if (this.active) {
          // 1. 清理依赖:通过 startTrack/endTack 机制清除当前 effect 上收集的所有依赖。
          // (这里的 startTrack(this) / endTack(this) 内部负责遍历并从 dep 中移除 effect)
          startTrack(this)
          endTack(this)
    
          // 2. 标记为非激活状态,阻止未来的 run() 执行依赖收集
          this.active = false
        }
      }
    }
    
  2. 返回 stop 函数
export function watch(
  source,
  cb,
  options: WatchOptions = EMPTY_OBJ
) {
  // ...

  /**
   * 🛑 返回给用户的停止监听函数
   */
  function stop() {
    effect.stop() // 调用 ReactiveEffect 实例的 stop 方法
  }

  return stop // 将停止函数返回
}

🔧 options 参数处理

  1. 从 options 中解构出所需的配置项:
    export function watch(
      source,
      cb,
      options: WatchOptions = EMPTY_OBJ
    ) {
      const { immediate, once, deep } = options
      // ... (其他 watch 逻辑)
    }
    

🚀 immediate (立即执行)

// ... (定义 job() 和 effect 实例后)

if (immediate) {
// 立即执行 job,cb(newValue, undefined)
  job()
}
else {
// 默认行为:首次执行只收集依赖并获取 oldValue
  oldValue = effect.run()
}

// ...

♻️ once (只执行一次)

// ... (options 解构后)
if (once) {
  const _cb = cb // 存储原始回调
  // 重新赋值 cb 为包装后的函数
  cb = (...args: any[]) => {
    _cb(...args) // 1. 执行原始回调
    stop() // 2. 自动调用 stop 停止监听
  }
}

// ... (watch 逻辑继续,确保 stop 函数在 watch 作用域内可用)

🧭 deep (深度监听)

effect 首次运行时,递归地访问被监听对象的所有嵌套属性,从而触发所有深层属性的 getter,将它们全部收集为依赖。

import { isObject } from '@vue/shared'

/**
 * 递归遍历对象,触发所有属性的 getter
 * @param value 待遍历的值
 * @param depth 剩余递归深度 (Infinity 表示无限制)
 * @param seen 用于处理循环引用的 Set
 */
function traverse(value, depth: number = Infinity, seen = new Set()) {
  // 边界条件 1: 不是对象,停止
  if (!isObject(value))
    return value

  // 边界条件 2: 已经访问过,停止 (防止循环引用)
  if (seen.has(value))
    return value

  // 边界条件 3: 深度用尽,停止
  if (depth <= 0)
    return value

  seen.add(value)
  depth-- // 消耗一层深度

  // 递归遍历所有 key
  for (const key in value) {
    // 访问 value[key],触发 getter 并收集依赖
    traverse(value[key], depth, seen)
  }

  return value // 返回原始值
}

export function watch(
  source,
  cb,
  options: WatchOptions = EMPTY_OBJ
) {
  const { immediate, once, deep } = options

  // ... (处理 source 转换为基础 getter 的逻辑)

  // 深度处理
  if (deep) {
    const baseGetter = getter // 存储原始 getter

    // 确定递归深度
    // true 表示无限深度,数字表示指定层级
    const depth = deep === true ? Infinity : deep

    // 重新定义 getter:执行原始 getter 并对返回值进行深度遍历
    getter = () => traverse(baseGetter(), depth)
  }

  // ... (创建 effect, job, stop, immediate 逻辑)
}

💡 Vue 3.5 层级控制: 通过将 deep 设置为数字(如 { deep: 2 }),可以精细控制 traverse 的递归深度,例如只监听两层嵌套属性的变化,以优化性能。