Watch 监听全景
聚焦
src/reactivity/watch/core.ts与utils.ts,梳理 watch 如何在 ref、getter 与响应式对象之间建立订阅关系,并保障清理与深度追踪的一致行为。
设计目标
- 统一入口:
watch(source, callback, options)接受Ref、getter 或普通对象,内部统一转换为 getter,屏蔽外部差异。 - 惰性触发:默认仅在依赖变化时调度回调;
immediate: true可在创建阶段先执行一次。 - 深度可控:通过
resolveDeepOption()结合显式参数与源特性,决定是否递归遍历嵌套字段。 - 清理闭环:回调拿到
onCleanup()可以注册销毁逻辑,watch 停止或下一次触发前都会自动执行。
核心组成
runner = new ReactiveEffect(getter, scheduler):封装底层依赖收集,并把调度交给runWatchJob(),保证依赖变动时统一走一套比较与清理流程。resolveDeepOption():推导WatchOptions.deep最终值。getter/ref 默认浅监听,响应式对象推断为深度,普通对象遵循显式布尔值。createGetter():根据WatchSource类型生成真正执行的 getter;深度模式下会调用traverse()递归访问所有字段以触发依赖。traverse():使用Set记录已访问节点,防止循环引用;遇到 ref 会继续向内读取其value。
调度流程
- 创建
ReactiveEffect,调度器只负责调用runWatchJob(),避免在依赖触发时直接执行回调。 - 首次进入根据
immediate决定:立即执行则直接跑一轮runWatchJob();否则先调用runner.run()把oldValue与hasOldValue初始化。 - 依赖变动时,
runWatchJob()会:- 检查 effect 是否仍活跃,失活直接跳出。
- 通过
runner.run()拿到新值并重建依赖。 - 在浅监听里使用
Object.is比较,防止相同引用触发回调。 - 如存在
cleanup先执行,再调用用户回调并传入onCleanup()注册新清理逻辑。 - 更新
oldValue与hasOldValue,供下一次比较与回调使用。
清理与生命周期
onCleanup(fn):回调内部调用后会记录本轮清理函数,在下次runWatchJob()前或手动stop()时触发。stop():显式返回给用户的停止函数,调用后 effect 失活并立刻运行末次清理。- 父级 effect 关联:若在 effect 内部创建 watch,会通过
effectStack.current.registerCleanup(stop)将停止逻辑挂到父 effect,确保作用域销毁时同步终止。 - effect scope:若当前存在
effectScope(组件 setup 或用户显式创建),recordEffectScope()与recordScopeCleanup()会把底层ReactiveEffect与stop句柄一起纳入 scope,scope.stop()时即可自动终止 watch 并触发最后一次onCleanup。
深度监听策略
- 函数源与 ref:默认浅监听,只在返回值或
value发生变化时触发;设置deep: true会进入traverse()。 - 响应式对象:若未显式传参,自动推导为深度监听,
traverse()会访问每个嵌套属性,从而订阅整棵树。 - 普通对象:非响应式对象不会参与依赖追踪,但在深度模式下
traverse()仍会执行一次,适用于与reactive()配合前的过渡场景。
使用与调试提示
- 若回调迟迟不触发,检查 getter 内是否真正读取了响应式字段;watch 只会追踪 getter 执行时访问到的依赖。
- 清理函数适合处理副作用(如事件监听、网络请求),避免重复触发造成资源泄漏。
- 在一次 effect 中反复创建 watch 时,务必把
stop注册到父作用域或显式调用,防止多余的副作用常驻。 - 深度监听对大型对象开销较高,可结合
getter精准返回所需字段,或配合浅监听提升性能。