响应式:已有对象属性的依赖收集

先理解原理和示例,再练习完整口述与追问。

getter 收集、setter 触发、分支依赖清理、停止订阅。

简化响应式先定义当前活跃 effect,读取属性时登记,写入变化时触发;实际系统还需要依赖清理、嵌套 effect、循环保护、调度、代理缓存与集合处理。若分支变化后不清理旧依赖,effect 会继续被不再使用的字段触发。

effect 读取 enabled?left:right:初次 enabled 为 true 时依赖 left,切为 false 后应不再因 left 改变而触发。嵌套 effect 时需要恢复外层收集目标;同一次写入若集合遍历期间又重新订阅,也需避免无限重复。

教学实现不等于 Vue 完整源码。只解释 Object.defineProperty 或 Proxy,不解释依赖如何建立与失效,原理仍不完整。

用 activeEffect 记录当前执行函数,getter 把它放进属性依赖集合,setter 变化后重新执行集合里的函数。每次执行先清理旧依赖,再收集本轮依赖,解决条件分支切换后的无效订阅。下面是同步教学实现,支持普通对象的已有、可写、可配置、可枚举字符串数据属性,不实现数组、动态增删、批量调度和完整 Vue 生命周期。

js
let activeEffect = null
const observedObjects = new WeakSet()

function effect(fn) {
  function cleanup() {
    for (const dep of runner.deps) dep.delete(runner)
    runner.deps.length = 0
  }
  function runner() {
    if (!runner.active) return fn()
    cleanup()
    const previous = activeEffect
    activeEffect = runner
    try { return fn() }
    finally { activeEffect = previous }
  }
  runner.deps = []
  runner.active = true
  runner.stop = () => { cleanup(); runner.active = false }
  runner()
  return runner
}

function observe(object) {
  if (object === null || typeof object !== 'object') return object
  const prototype = Object.getPrototypeOf(object)
  if (Array.isArray(object) ||
      (prototype !== Object.prototype && prototype !== null)) {
    throw new TypeError('仅支持普通对象')
  }
  if (observedObjects.has(object)) return object
  observedObjects.add(object)
  for (const key of Object.keys(object)) {
    const descriptor = Object.getOwnPropertyDescriptor(object, key)
    if (!descriptor.configurable || !descriptor.writable || !('value' in descriptor)) {
      throw new TypeError('属性必须是可写、可配置的数据属性')
    }
    let value = observe(descriptor.value)
    const dep = new Set()
    Object.defineProperty(object, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (activeEffect && !dep.has(activeEffect)) {
          dep.add(activeEffect)
          activeEffect.deps.push(dep)
        }
        return value
      },
      set(next) {
        if (Object.is(next, value)) return
        value = observe(next)
        for (const runner of [...dep]) {
          if (runner !== activeEffect) runner()
        }
      }
    })
  }
  return object
}

示例:const state=observe({flag:true,a:1,b:2}),执行 effect(()=>state.flag?state.a:state.b),切换 flag 后不再订阅 a。返回的 runner.stop() 可取消自动追踪。同步触发不包含 Vue 的批量去重;相互写入造成的循环副作用也不自动解决。初始化与属性数相关,单次触发与依赖及执行工作量相关;应只传可写数据对象,不用于只读或带复杂描述符的实例。