vue3源码解析:watchEffect原理

194 阅读6分钟

一、前言

在上文中,我们分析了 Vue3 的 watch API 实现。本文我们将分析另一个重要的响应式 API —— watchEffect。与 watch 不同,watchEffect 不需要指定监听的数据源,它会自动追踪回调函数中使用的所有响应式依赖。

二、示例引入

让我们从一个简单的例子开始:

<script setup>
import { ref, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

// 自动追踪 count 和 message 的变化
watchEffect(() => {
  console.log(`Count: ${count.value}, Message: ${message.value}`)
})

// 支持清理副作用
watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    console.log(count.value)
  }, 1000)
  
  // 在下次执行或停止时清理
  onCleanup(() => clearInterval(timer))
})

// 控制执行时机
watchEffect(() => {
  console.log('DOM updated:', count.value)
}, { flush: 'post' })
</script>

这个例子展示了 watchEffect 的几个重要特性:

  1. 自动依赖追踪:不需要显式指定依赖
  2. 清理机制:支持在重新执行前清理副作用
  3. 执行时机控制:可以控制副作用函数的执行时机

三、核心实现分析

3.1 watchEffect 的入口

watchEffect 的实现相对简单,它是对 doWatch 函数的封装:

export function watchEffect(
  effect: WatchEffect,
  options?: WatchEffectOptions,
): WatchHandle {
  return doWatch(effect, null, options)
}

export function watchPostEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
): WatchHandle {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
  )
}

export function watchSyncEffect(
  effect: WatchEffect,
  options?: DebuggerOptions,
): WatchHandle {
  return doWatch(
    effect,
    null,
    __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },
  )
}

3.2 doWatch 的实现

doWatch 函数是 watch 和 watchEffect 的统一实现。与 watch 不同,watchEffect 在调用时传入的 cb 参数为 null,这使得它走向了不同的实现路径:

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,  // watchEffect 传入 null
  options: WatchOptions = EMPTY_OBJ,
): WatchHandle {
  // 1. 处理选项
  const { immediate, deep, flush, once } = options
  
  // 2. 开发环境警告
  if (__DEV__ && !cb) {
    if (immediate !== undefined) {
      warn(
        `watch() "immediate" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`,
      )
    }
    if (deep !== undefined) {
      warn(
        `watch() "deep" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`,
      )
    }
  }

  // 3. 配置调度器
  let isPre = false
  if (flush === 'post') {
    baseWatchOptions.scheduler = job => {
      queuePostRenderEffect(job, instance && instance.suspense)
    }
  } else if (flush !== 'sync') {
    // default: 'pre'
    isPre = true
    baseWatchOptions.scheduler = (job, isFirstRun) => {
      if (isFirstRun) {
        job()
      } else {
        queueJob(job)
      }
    }
  }

  // 1. 处理 source 参数
  let getter: () => any
  if (!cb) {
    // watchEffect 的情况:source 本身就是 effect 函数
    getter = () => {
      // 执行 source 函数时会自动收集依赖
      if (cleanup) {
        cleanup()
      }
      return source(onCleanup)
    }
  } else {
    // watch 的情况:需要手动指定监听源
    getter = () => traverse(source)
  }

  // 2. 创建 effect 实例
  const effect = new ReactiveEffect(getter)
  
  // 5. 配置调度器
  effect.scheduler = scheduler
    ? () => scheduler(job, false)
    : (job as EffectScheduler)

  // 6. 首次执行,收集依赖
  if (cb) {
    if (immediate) {
      job(true)
    } else {
      oldValue = effect.run()
    }
  } else {
    // watchEffect 的情况:直接执行 effect
    effect.run()
  }

  // 7. 返回停止函数
  return () => {
    effect.stop()
    cleanup && cleanup()
  }
}

3.2.1 自动依赖收集原理

watchEffect 的自动依赖收集主要通过以下机制实现:

  1. 直接使用回调函数作为 getter
// watchEffect 的情况
getter = () => {
  if (cleanup) {
    cleanup()
  }
  return source(onCleanup)
}
  • 不需要像 watch 那样手动指定数据源
  • 回调函数中访问的响应式数据会被自动收集为依赖
  1. 依赖收集过程
// ReactiveEffect 的 run 方法
run() {
  // 设置当前活跃的 effect
  activeEffect = this
  
  try {
    // 执行 getter 函数,此时会访问响应式数据
    // 响应式数据的 get 操作会自动收集当前的 activeEffect
    return this.fn()
  } finally {
    activeEffect = undefined
  }
}
  1. 响应式数据的依赖收集
// ref 的实现
class RefImpl {
  get value() {
    track(this, TrackOpTypes.GET, 'value')
    return this._value
  }
}

// reactive 的实现
function createGetter() {
  return function get(target, key) {
    const res = Reflect.get(target, key)
    track(target, TrackOpTypes.GET, key)
    return res
  }
}

这种设计带来的好处是:

  1. 使用更简单:不需要显式声明依赖项
  2. 依赖自动管理:依赖会随着回调函数的执行自动收集和清理
  3. 更少的心智负担:不用担心遗漏某个依赖项

举例说明:

const count = ref(0)
const message = ref('Hello')

// 不需要显式指定监听 count 和 message
watchEffect(() => {
  // 访问 count.value 和 message.value 时会自动收集依赖
  console.log(`${count.value}: ${message.value}`)
})

// 相比之下,watch 需要显式指定依赖源
watch([count, message], ([newCount, newMessage]) => {
  console.log(`${newCount}: ${newMessage}`)
})

3.2.2 watchEffect 的本质设计

watchEffect 的一个重要设计思想是: 它本质上就是把传入的 source 函数同时作为依赖收集的 getter 和响应变化的回调函数。这种设计带来了几个优点:

  1. 简化心智模型
// watch 需要分别指定数据源和回调
watch(source, (newValue) => {
  // 处理变化
})

// watchEffect 统一在一个函数中完成
watchEffect(() => {
  // 访问数据的同时也处理变化
})
  1. 统一职责

    • 不需要分离"要监听什么"和"数据变化后要做什么"
    • 在同一个上下文中处理相关的逻辑,提高了代码的内聚性
    • 减少了可能的逻辑分散和依赖管理的复杂性
  2. 更符合直觉

const count = ref(0)
const message = ref('Hello')

// 直接描述"我要做什么",而不是"我要监听什么,然后做什么"
watchEffect(() => {
  console.log(`当前状态: ${count.value}, ${message.value}`)
})

这种设计反映了 Vue3 的一个重要理念: 通过统一和简化 API 的设计,来降低开发者的使用成本。watchEffect 通过将 source 函数同时作为 getter 和 callback,巧妙地实现了这一目标。

3.3 执行时机控制

watchEffect 支持三种执行时机:

  1. pre(默认) :组件更新前执行
// 默认 pre 模式
baseWatchOptions.scheduler = (job, isFirstRun) => {
  if (isFirstRun) {
    job()
  } else {
    queueJob(job)
  }
}
  1. post:组件更新后执行
// post 模式
baseWatchOptions.scheduler = job => {
  queuePostRenderEffect(job, instance && instance.suspense)
}
  1. sync:同步执行
// sync 模式不使用调度器,直接执行
if (flush === 'sync') {
  // 直接执行,不进入队列
  effect.run()
}

3.4 清理机制

watchEffect 提供了清理机制,用于清理副作用:

watchEffect((onCleanup) => {
  // 注册清理函数
  onCleanup(() => {
    // 清理逻辑
  })
})

清理函数的实现:

const cleanupMap: WeakMap<ReactiveEffect, (() => void)[]> = new WeakMap()

export function onWatcherCleanup(
  cleanupFn: () => void,
  failSilently = false,
  owner: ReactiveEffect | undefined = activeWatcher
): void {
  if (owner) {
    let cleanups = cleanupMap.get(owner)
    if (!cleanups) cleanupMap.set(owner, (cleanups = []))
    cleanups.push(cleanupFn)
  }
}

3.5 停止监听

watchEffect 返回一个停止函数,用于停止监听:

const stop = watchEffect(() => {
  /* ... */
})

// 停止监听
stop()

停止函数的实现:

const watchHandle = () => {
  effect.stop()
  cleanup && cleanup()
}

watchHandle.pause = effect.pause.bind(effect)
watchHandle.resume = effect.resume.bind(effect)
watchHandle.stop = watchHandle

return watchHandle

四、总结

通过以上分析,我们了解了 watchEffect 的核心实现:

  1. 简化的 API

    • 不需要显式指定依赖
    • 自动追踪响应式依赖
    • 更简洁的使用方式
  2. 灵活的执行控制

    • 支持三种执行时机
    • 可以控制副作用的执行顺序
    • 提供暂停和恢复功能
  3. 完善的清理机制

    • 支持副作用清理
    • 自动在重新执行前调用清理函数
    • 停止监听时也会执行清理

与 watch 相比,watchEffect 的特点是:

  1. watch

    • 需要明确指定监听的数据源
    • 可以访问新值和旧值
    • 支持深度监听选项
  2. watchEffect

    • 自动收集依赖
    • 立即执行
    • 更适合处理副作用

在实际开发中,我们应该根据具体场景选择合适的 API:

  1. 需要比较新旧值时,使用 watch
  2. 需要执行副作用时,使用 watchEffect
  3. 需要控制执行时机时,使用对应的 watchEffect 变体