zhangdizhangdi

响应式系统(reactivity)

开始

官网

源码

重点

  • reactive() / ref() / computed() 的实现
  • 依赖收集 (track) 和 触发更新 (trigger)
  • effect() 的运行机制(副作用函数调度)
  • scheduler(任务调度队列)
  • 浅层/只读/深层响应式的区别

三个核心角色

  • 依赖容器 Dep:本质是 Set,记录“谁在用这个数据”。
  • 依赖图 targetMap:WeakMap<object, Map<key, Dep>>,按“对象 → key → 订阅它的副作用集合”组织。
  • 副作用 ReactiveEffect:包装你的副作用函数(渲染/打印/计算等),记录它“订阅了哪些 dep”。

两条关键链路

  • 读取链:读取 state.a → Proxy get → track(target,key) → 把当前活跃的 ReactiveEffect 放入该 key 的 Dep。
  • 写入链:写入 state.a = 2 → Proxy set → trigger(target,key) → 取出 Dep,把里面的 ReactiveEffect 重新执行(或交给 scheduler)。

关键文件(用于断点)

  • packages/reactivity/src/effect.ts:ReactiveEffect、track、trigger
  • packages/reactivity/src/baseHandlers.ts:对象/数组的 get/set 拦截
  • packages/reactivity/src/collectionHandlers.ts:Map/Set 等集合类型拦截
  • packages/reactivity/src/ref.ts:ref/shallowRef
  • packages/reactivity/src/computed.ts:computed(带缓存与失效标记 dirty)

reactive

js
const state = reactive({ count: 0 })

创建

  1. reactive(obj) → 调用 createReactiveObject
  2. 返回一个 Proxy,拦截 get/set/delete 操作(位于 baseHandlers.ts)

依赖收集(读取时)

  1. 读取 state.count → Proxy get
  2. 调用 track(target, key):
    1. 找到 targetMap.get(state),再找 depsMap.get(‘count’)
    2. 把当前活跃的 ReactiveEffect 加入 dep

触发更新(写入时)

  1. Proxy set → 调用 trigger(target, key)
  2. 从 targetMap 拿到对应的 dep
  3. 遍历 dep 中的所有 effect,依次执行或交给 scheduler

关键点

  • reactive 基于 Proxy
  • 按 “对象 → key → effect 集合” 组织依赖
  • 依赖清理保证分支切换时不会泄漏

ref

js
const count = ref(0)

创建

  • ref(value) → 生成 RefImpl 类实例
  • 内部持有一个 _value 和一个 dep: Set

依赖收集

  • 访问 count.value → get value
  • 调用 trackRefValue(this) → 把活跃 effect 加入 dep

触发更新

  • set value(newVal) → 先判断是否真的变化
  • 调用 triggerRefValue(this) → 遍历 dep 执行所有 effect

关键点

  • ref 针对原始值(不需要 Proxy)
  • 只有 .value 属性做依赖收集和触发
  • 对象 ref 内部会自动 reactive 化

computed

js
const double = computed(() => state.count * 2)

创建

  1. computed(getter) → 创建一个 ComputedRefImpl
  2. 内部持有一个 lazy effect(不会立即执行)
  3. 有 dirty 标记(初始为 true),和 value 缓存

依赖收集

第一次访问 double.value:

  • 查 dirty → 为 true
  • 执行内部 effect,计算新值,并收集它依赖的 reactive/ref
  • dirty = false
  • 把 ComputedRefImpl 自己作为 依赖源,track 到外部 effect

再次读取

  • 下次 double.value → 检查 dirty → 重新执行 getter → 更新缓存

触发更新

  • state.count 更新 → 触发 trigger → 通知内部 effect 的 scheduler
  • scheduler 不直接执行 getter,而是:
    • 标记 dirty = true
    • 调用 triggerRefValue(this) → 通知依赖 double 的外部 effect(渲染等)

关键点

  • computed = 带缓存的派生值
  • 不会每次都计算,只有依赖变更后,下一次访问才重新计算
  • 部分成两层依赖链:
    • getter 依赖 → state
    • 外部 effect 依赖 → computed
js
reactive(obj)       ref(val)                computed(getter)
    ↓                   ↓                           ↓
 Proxy 包装        RefImpl 包装            ComputedRefImpl 包装
    ↓                   ↓                           ↓
 gettrack      get valuetrack         get valuedirty?
 settrigger    set valuetrigger           truerun effect
                                                  false缓存

侦听器 watchers

watch

ts
watch(source, callback, options?)
  1. watch 内部会创建一个 ReactiveEffect,把 source 包装成 getter。
  2. 每次依赖变更时,effect 触发 → 对比新旧值 → 执行回调。
  3. onCleanup 提供“副作用清理”,适合防抖/请求取消。

watchEffect

ts
watchEffect(effectFn, options?)
  • 不需要手动指定 source
  • 直接把函数作为副作用执行 → 内部自动收集依赖
  • 类似于 effect + watch 的结合体

调试重点

  • watch
    • packages/runtime-core/src/apiWatch.ts
    • 关键函数:
      • doWatch(source, cb, options)(watch / watchEffect 都会走这里)
      • scheduler:触发时机控制
      • job:回调包装函数(包含新旧值、cleanup)
  • watchEffect
    • 也是调用 doWatch,只是没有 cb(内部 effect 本身就是回调)
js
keyword.value = 'hi'

Proxy.settrigger()

ReactiveEffect.schedulerqueueJob()

flushJobs() → 执行 watch job

执行回调 cb(newVal, oldVal)