一、写在开头
前端框架的核心任务是将数据映射为 DOM,并在数据变化时自动更新视图。响应式系统正是实现这一自动更新的基础——当应用状态改变时,依赖该状态的视图、计算属性、侦听器等能够自动重新执行。

Vue 2 使用 Object.defineProperty 劫持对象属性的 getter/setter,配合观察者模式实现了响应式。但它存在几个难以逾越的局限:无法检测属性的添加或删除;对数组的变异方法需要额外处理;初始化时需递归遍历所有属性,存在性能开销。

Vue 3 基于 ES2015 的 Proxy 实现了全新的响应式系统,不仅完美解决了上述问题,还带来了更好的性能、更简洁的代码结构,同时支持 Map、Set 等复杂数据类型。本文将结合 packages/reactivity 目录下的源码,深度剖析这一系统的设计与实现,并手写一个 mini 版巩固理解。


二、响应式系统的核心:Proxy vs Object.defineProperty

2.1 Object.defineProperty 的工作原理

Object.defineProperty(obj, key, descriptor) 可以精确控制对象属性的读取和赋值行为:

javascript

let obj = {}
let val = 1
Object.defineProperty(obj, 'a', {
  get() {
    console.log('get a')
    return val
  },
  set(newVal) {
    console.log('set a')
    val = newVal
  }
})
obj.a    // 触发 get
obj.a = 2 // 触发 set

Vue 2 通过递归遍历对象的所有属性,为每个属性添加 getter 和 setter。在 getter 中收集依赖(Watcher),setter 中触发更新。但这种方式存在明显局限:

  • 无法监听属性新增/删除obj.newKey = 'value' 不会触发 setter,因此 Vue 2 提供了 Vue.set/delete

  • 无法直接监听数组索引和 length 变化:需要重写数组的 7 种变异方法(push/pop/shift/unshift/splice/sort/reverse)。

  • 初始化时需要深度递归,影响启动性能,且对于深层嵌套的对象可能造成不必要的观察。

简单模拟 Vue 2 响应式:

javascript

function defineReactive(obj, key) {
  let val = obj[key]
  const dep = [] // 依赖列表(简化)
  Object.defineProperty(obj, key, {
    get() {
      if (currentEffect) dep.push(currentEffect)
      return val
    },
    set(newVal) {
      val = newVal
      dep.forEach(fn => fn())
    }
  })
}
// 需要递归遍历
function observe(obj) {
  Object.keys(obj).forEach(key => defineReactive(obj, key))
}

2.2 Proxy 的工作原理

Proxy 可以创建一个对象的代理,拦截几乎所有基本操作:

javascript

const p = new Proxy(target, {
  get(target, key, receiver) { /* ... */ },
  set(target, key, value, receiver) { /* ... */ },
  deleteProperty(target, key) { /* ... */ },
  // 共支持13种拦截方法
})

Vue 3 利用 Proxy 的 getsetdeletePropertyhasownKeys 等拦截器,能够全面捕获对数据的操作。其优势包括:

  • 可监听属性的增删set 拦截器可感知新增属性,deleteProperty 拦截删除操作。

  • 自然支持数组:直接拦截 arr[0]arr.length 等操作,无需重写数组方法。

  • 懒代理:只在读取某个属性且其值是对象时才进行递归代理,提升初始化性能。

  • 可代理更多数据类型:Map、Set、WeakMap、WeakSet 等。

简单 Proxy 示例:

javascript

const handler = {
  get(target, key) {
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler) // 懒代理
    }
    return target[key]
  },
  set(target, key, value) {
    target[key] = value
    console.log('更新视图')
    return true
  }
}
const data = new Proxy({ a: 1, arr: [1,2,3] }, handler)
data.a = 2    // 触发更新
data.b = 3    // 触发更新(新增属性)
data.arr.push(4) // 触发更新

2.3 两者的详细对比

对比维度Object.definePropertyProxy
功能完整性无法监听属性增删、数组索引和 length可拦截13种操作,覆盖所有数据变更场景
性能初始化需递归遍历,内存消耗大懒代理,按需递归,整体更高效
易用性需要额外 API(set/set/delete),数组特殊处理统一拦截,代码简洁
兼容性支持 IE9+,生态成熟不支持 IE,但现代浏览器全面支持
数据结构支持仅限普通对象支持数组、Map、Set、WeakMap、WeakSet

Vue 3 选择 Proxy 正是因为它提供了更完整的拦截能力和更友好的开发体验。虽然放弃了 IE 兼容性,但顺应了现代前端发展趋势,也使得响应式系统的实现代码量大幅减少。


三、Vue 3 响应式系统的整体架构

Vue 3 响应式系统的核心模块均位于 packages/reactivity/src 目录下,主要包括:

  • reactive:将普通对象转换为响应式代理

  • ref:包装基本类型值(或对象),使其变为响应式

  • effect:创建副作用函数,并在其依赖变化时自动重新执行

  • computed:基于其他响应式数据计算得出新值,具有懒执行和缓存特性

  • watch/watchEffect:侦听响应式数据的变化并执行回调

整体工作流程如下:

text

[应用数据]
   │
   ├─ reactive(obj) / ref(val)  创建响应式数据
   │
   └─> effect / computed / watch  创建副作用,依赖响应式数据
         │
         ▼
   Proxy get 拦截  ──── track(收集依赖)────> 存储到 targetMap (WeakMap)
                                                     │
   Proxy set/delete 拦截 ─── trigger(触发依赖)────> 取出对应 effect 重新执行
                                                     │
                                                     ▼
                                              [异步更新队列] -> 执行副作用 -> 更新 DOM

核心概念区分:

  • 响应式数据:被 Proxy 代理后的对象或通过 ref 包装的值,能够被“追踪”变化。

  • 副作用函数(effect):依赖响应式数据的函数,当数据变化时会被重新调用。组件的渲染函数就是典型的副作用。

整个系统围绕“依赖收集”和“触发更新”两个阶段运转,接下来我们将深入源码逐一剖析。


四、依赖收集的完整实现

4.1 依赖收集的基本原理

依赖 是指某个副作用函数读取了哪些响应式数据。当数据变化时,需要通知这些副作用重新执行。依赖收集发生在响应式数据的 get 拦截 阶段。

Vue 3 使用一个三层结构来存储依赖关系:

text

targetMap: WeakMap<target, depsMap>
    depsMap: Map<key, dep>
        dep: Set<ReactiveEffect>
  • targetMap 是一个 WeakMap,键为原始目标对象,值为 depsMap。使用 WeakMap 利于垃圾回收。

  • depsMap 是一个 Map,键为目标对象的具体属性名,值为 dep

  • dep 是一个 Set,存放所有与该属性关联的 ReactiveEffect(副作用对象)。Set 确保依赖不重复。

4.2 核心源码分析

reactive 函数 (packages/reactivity/src/reactive.ts

javascript

export function reactive(target) {
  // 如果目标已是只读代理或响应式代理,直接返回
  if (isReadonly(target)) return target
  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap)
}

createReactiveObject 核心逻辑:

javascript

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  // 非对象类型直接返回
  if (!isObject(target)) return target
  // 如果已经代理过,返回缓存的代理(proxyMap 防止重复代理)
  const existingProxy = proxyMap.get(target)
  if (existingProxy) return existingProxy
  // 根据类型选择处理器:普通对象用 baseHandlers,集合类型用 collectionHandlers
  const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
  proxyMap.set(target, proxy)
  return proxy
}

Proxy 的 get 拦截器 实现在 baseHandlers 中(packages/reactivity/src/baseHandlers.ts),核心是调用 track 收集依赖:

javascript

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    // 处理各种内部 key(如 __v_isReactive、__v_raw 等)...
    const res = Reflect.get(target, key, receiver)
    // 非只读时进行依赖收集
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // 若值是对象且非浅层代理,递归转换为响应式(懒代理)
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

track 函数 详细解析(packages/reactivity/src/effect.ts

javascript

export function track(target, type, key) {
  // 当前激活的 effect 存在时才收集
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }
    // 将当前 effect 加入依赖集合
    trackEffects(dep)
  }
}

function trackEffects(dep) {
  // 避免重复收集:判断 effect 的 deps 数组是否已包含此 dep
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep) // effect 也反向记录该 dep,便于清理
  }
}

优化策略:

  • activeEffect 确保只有当前正在执行的副作用才会被收集,避免无关读取触发收集。

  • 双向记录(effect 记录它依赖的所有 dep,dep 记录所有订阅的 effect),使得 effect 重新执行时可以清理过期的依赖。

  • 使用位运算 trackOpBit 进行递归跟踪的控制,防止深层嵌套下的重复收集。

4.3 ref 的特殊处理

ref 用于让基本类型值(或对象)具备响应式。对象类型 ref 内部会调用 reactive 转换 .value 的值。

ref 的实现 (packages/reactivity/src/ref.ts

javascript

export function ref(value) {
  return createRef(value, false)
}

function createRef(rawValue, shallow) {
  if (isRef(rawValue)) return rawValue
  return new RefImpl(rawValue, shallow)
}

class RefImpl {
  constructor(value, __v_isShallow) {
    this._rawValue = value
    this._value = __v_isShallow ? value : toReactive(value) // 对象转 reactive
    this.__v_isRef = true
  }
  get value() {
    trackRefValue(this) // 收集依赖
    return this._value
  }
  set value(newVal) {
    if (hasChanged(this._rawValue, newVal)) {
      this._rawValue = newVal
      this._value = toReactive(newVal)
      triggerRefValue(this) // 触发更新
    }
  }
}

ref 依赖收集直接通过 trackRefValue 将自身作为一个整体收集,而不是按属性。其依赖存储在 dep 属性(Set)中。与 reactive 相比,ref 更轻量,适合单一值的响应式包裹,且可通过 .value 直接替换整个对象。


五、触发更新的完整实现

5.1 触发更新的基本原理

触发更新发生在响应式数据的 set 或 deleteProperty 拦截 阶段。当数据变更时,根据修改类型(新增、修改、删除)从 targetMap 中找到对应的 dep,并执行所有关联的副作用。

为提升性能,Vue 3 不会立即同步执行副作用,而是将副作用放入一个异步更新队列,在下一个 tick 统一执行,从而批量处理更新,避免不必要的重复计算。

5.2 核心源码分析

set 拦截器 (packages/reactivity/src/baseHandlers.ts

javascript

function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    // 判断操作类型:新增(ADD)还是修改(SET)
    const hadKey = hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // 若 receiver 是 target 的代理且未被屏蔽,触发更新
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

trigger 函数 详细解析(packages/reactivity/src/effect.ts

javascript

export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  let deps = []
  // 获取与 key 直接相关的依赖
  if (key !== undefined) {
    deps.push(depsMap.get(key))
  }
  // 处理 ADD / DELETE / SET 等特殊操作(如数组 length 变更、迭代器相关)
  switch (type) {
    case TriggerOpTypes.ADD:
      // 对数组新增,需要触发 length 相关的依赖
      if (isArray(target)) {
        deps.push(depsMap.get('length'))
      }
      // ...
      break
    case TriggerOpTypes.DELETE:
      // ...
      break
    case TriggerOpTypes.SET:
      // ...
      break
  }
  // 合并所有需要执行的 effect
  const effects = []
  for (const dep of deps) {
    if (dep) {
      effects.push(...dep)
    }
  }
  // 通过调度器执行副作用(加入更新队列)
  triggerEffects(createDep(effects))
}

function triggerEffects(dep) {
  for (const effect of dep) {
    // 若 effect 有 scheduler,则使用 scheduler 调度执行
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

不同操作类型的处理:新增属性 除了触发该属性的依赖,还会触发与迭代(如 Object.keys)相关的依赖;删除属性 同理;对于数组,修改 length 或通过索引设置可能触发多个依赖。

5.3 异步更新队列

当副作用被触发时,不会立即执行 effect.run(),而是推入一个队列 queue,并交由 scheduler 调度。这样同一个 Tick 内的多次数据变更只会触发一次副作用刷新。

nextTick 的实现(packages/runtime-core/src/scheduler.ts

javascript

const queue = []
let isFlushing = false
const resolvedPromise = Promise.resolve()

export function queueJob(job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    resolvedPromise.then(flushJobs)
  }
}

function flushJobs() {
  try {
    for (let i = 0; i < queue.length; i++) {
      const job = queue[i]
      job()
    }
  } finally {
    isFlushing = false
    queue.length = 0
  }
}

nextTick 实际上就是 Promise.resolve().then(callback)(在 Vue 3 中优先使用微任务),等待当前宏任务执行完毕后清空队列。

批量更新的优化

  • 同一个 effect 若被多次触发,只会入队一次(通过 queue.includes 判重)。

  • 允许提供 scheduler,例如组件的渲染函数会使用自定义 scheduler,确保渲染过程高效批处理。


六、computed 的实现原理

6.1 computed 的基本概念和特性

计算属性接收一个 getter 函数,返回一个只读的响应式 ref 对象。它具有两大核心特性:

  • 懒执行:只有在被读取时才会计算值,若没有依赖项变化,不会主动重新计算。

  • 缓存:依赖项不变时多次读取会立即返回上一次的计算结果,而不会重复执行 getter。

  • 可写计算属性:若提供 setter,则可通过赋值触发展更新。

6.2 核心源码分析

computed 函数 (packages/reactivity/src/computed.ts

javascript

export function computed(getterOrOptions) {
  let getter, setter
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = () => warn('Write operation failed: computed value is readonly')
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set)
}

ComputedRefImpl 类

javascript

class ComputedRefImpl {
  constructor(getter, _setter, isReadonly) {
    this._setter = _setter
    this._dirty = true       // 脏标记,控制是否需要重新计算
    this._cacheable = true   // 是否启用缓存
    this.effect = new ReactiveEffect(getter, () => {
      // 当依赖变化时,不立即求值,仅标记为脏,并触发依赖此 computed 的 effect 更新
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // 通知上层订阅者
      }
    })
  }
  get value() {
    // 读取时进行依赖收集
    const self = toRaw(this)
    trackRefValue(self)
    // 如果脏或不需要缓存,则重新计算
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run() // 执行 getter,此时会收集依赖
    }
    return self._value
  }
  set value(newValue) {
    this._setter(newValue)
  }
}

懒执行与缓存机制

  • 内部创建了一个 ReactiveEffect,但不立即执行(effect 构造时 lazy 选项阻止自动运行)。

  • 依赖变更时,scheduler 仅将 _dirty 置为 true,并触发自身 triggerRefValue,通知依赖该 computed 的 effect 重新执行。

  • 真正求值发生在 get value() 中,且只有 _dirty 为 true 时才重新执行 getter,否则直接返回 _value

  • 当没有任何地方读取 computed 时,即使依赖变化也不会发生计算,这就是懒执行。

6.3 computed 与 effect 的区别

对比维度computedeffect
执行时机懒执行:只有被读取时才计算立即执行一次,之后依赖变化时自动重新执行
返回值返回一个 ref 对象,通过 .value 访问返回一个 runner 函数,无自动返回值概念
缓存依赖不变时缓存结果每次重新执行,无缓存
主要用途派生计算、模板中高效绑定执行副作用,如 DOM 更新、watch 回调
内部实现基于 effect,额外增加了脏检查与缓存纯粹的依赖收集与重新执行

使用场景:当需要基于现有数据产生新数据并需要缓存时,用 computed;当需要在数据变化时执行某些操作(如异步请求、DOM 操作)时,用 effect(或 watch)。


七、watch 的实现原理

7.1 watch 的基本概念和使用方式

Vue 3 提供了 watch 和 watchEffect 两个 API:

  • watchEffect:立即执行传入的函数,并自动追踪其中的响应式依赖,依赖变化时重新执行。

  • watch:显式指定监听一个或多个数据源,数据变化时触发回调,支持懒执行、获取旧值、深度监听等。

常用配置:

  • immediate: true:创建时立即执行回调。

  • deep: true:深度监听对象内部变化。

  • flush: 'pre' | 'post' | 'sync':控制回调的执行时机。

7.2 核心源码分析

watch 函数 和 watchEffect 最终都调用 doWatch (packages/reactivity/src/watch.ts

javascript

function watch(source, cb, options) {
  return doWatch(source, cb, options)
}
export function watchEffect(effect, options) {
  return doWatch(effect, null, options)
}

doWatch 核心流程:

  1. 规范化监听源source 可以是 ref、reactive 对象、getter 函数、数组等。

    • 若为 ref 或 reactive,会自动解包或深度追踪。

    • 若为函数,视为 getter,每次运行获取当前值。

    • 若为数组,组合多个源,回调参数为数组形式的新旧值。

  2. 创建 effect:根据配置创建 ReactiveEffectscheduler 中执行用户回调。

javascript

function doWatch(source, cb, { immediate, deep, flush } = {}) {
  // ... 规范化 source 为 getter
  let getter
  if (isRef(source)) {
    getter = () => source.value
  } else if (isReactive(source)) {
    getter = () => source
    deep = true // 默认对 reactive 对象开启深度监听
  } else if (isFunction(source)) {
    getter = source
  } // ...
  
  // 深度监听时,在 getter 内部递归读取属性,确保触发依赖收集
  if (deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  
  let oldValue = {}
  const job = () => {
    if (cb) {
      const newValue = effect.run()
      if (deep || hasChanged(newValue, oldValue)) {
        cb(newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup)
        oldValue = newValue
      }
    } else {
      effect.run() // watchEffect
    }
  }
  
  let scheduler
  if (flush === 'sync') {
    scheduler = job
  } else {
    scheduler = () => queuePreFlushCb(job) // 默认 pre 队列,DOM 更新前执行
  }
  
  const effect = new ReactiveEffect(getter, scheduler)
  if (cb) {
    if (immediate) job()
    else oldValue = effect.run() // 首次执行获取旧值
  } else {
    effect.run() // watchEffect 立即执行
  }
  return () => effect.stop() // 返回取消监听的函数
}

deep 实现traverse 函数递归访问对象的所有属性,触发 getter 从而收集所有深层依赖。这样任意层级的变化都能被感知。

flush 时机处理

  • pre:默认,回调在组件更新前执行(通过 queuePreFlushCb 加入 pre 队列)。

  • post:回调在组件更新后执行(通过 queuePostRenderEffect)。

  • sync:同步执行,数据变化立即触发回调。

7.3 watch 与 computed 的区别

对比维度watchcomputed
用途执行副作用(异步请求、DOM 操作)派生计算新值,主要用于模板绑定
执行默认懒执行(immediate 可改为立即)懒执行,依赖不变不计算
返回值返回停止监听函数返回只读 ref
缓存
访问新值回调参数提供新旧值通过 .value 获取当前计算结果
深度监听支持 deep 递归追踪依赖自动追踪,无需 deep
典型场景“当某个数据变化时,发起网络请求”“根据 firstName 和 lastName 计算 fullName”

两者在响应式系统中分工明确:computed 用于纯计算,watch 用于有副作用的响应


八、手写一个简化版的 Vue 3 响应式系统

我们将从零实现一个 mini 响应式系统,包括 reactiveeffectcomputedwatch

8.1 整体设计思路

核心模块:

  • reactive(target):返回 Proxy 代理。

  • effect(fn):立刻执行 fn,并追踪依赖。

  • track(target, key):在 get 中调用,收集当前 effect。

  • trigger(target, key):在 set 中调用,运行对应的 effect。

  • computed(getter):返回懒执行、有缓存的 ref。

  • watch(source, cb):监听 source 变化并执行 cb。

数据结构:targetMap(WeakMap) -> depsMap(Map) -> dep(Set)。

8.2 分步实现

javascript

// mini-reactive.js

let activeEffect = null
const targetMap = new WeakMap()

// ---- track & trigger ----
function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
  activeEffect.deps.push(dep) // 反向记录
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    // 使用新 Set 避免死循环(清理等)
    const effectsToRun = new Set(dep)
    effectsToRun.forEach(effect => {
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    })
  }
}

// ---- reactive ----
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      track(target, key)
      if (typeof res === 'object' && res !== null) {
        return reactive(res) // 懒代理
      }
      return res
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      if (oldValue !== value) {
        trigger(target, key)
      }
      return result
    }
  })
}

// ---- effect ----
class ReactiveEffect {
  constructor(fn, scheduler = null) {
    this.fn = fn
    this.scheduler = scheduler
    this.deps = []
  }
  run() {
    // 清理上次依赖,重新收集
    cleanupEffect(this)
    activeEffect = this
    const result = this.fn()
    activeEffect = null
    return result
  }
  stop() {
    cleanupEffect(this)
    this.active = false
  }
}

function cleanupEffect(effect) {
  effect.deps.forEach(dep => dep.delete(effect))
  effect.deps.length = 0
}

function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn, options.scheduler)
  _effect.run()
  const runner = _effect.run.bind(_effect)
  runner.effect = _effect
  return runner
}

// ---- ref ----
function ref(value) {
  return new RefImpl(value)
}
class RefImpl {
  constructor(value) {
    this._value = value
    this.dep = new Set()
  }
  get value() {
    if (activeEffect) {
      this.dep.add(activeEffect)
    }
    return this._value
  }
  set value(newVal) {
    if (newVal !== this._value) {
      this._value = newVal
      this.dep.forEach(effect => {
        if (effect.scheduler) effect.scheduler()
        else effect.run()
      })
    }
  }
}

// ---- computed ----
function computed(getter) {
  let dirty = true
  let value
  const _effect = new ReactiveEffect(getter, () => {
    // scheduler:依赖变化时标记脏,不立即求值
    if (!dirty) {
      dirty = true
      trigger(computedRef, 'value')
    }
  })
  const computedRef = {
    get value() {
      track(computedRef, 'value') // 收集 computed 的依赖
      if (dirty) {
        dirty = false
        value = _effect.run()
      }
      return value
    }
  }
  return computedRef
}

// ---- watch ----
function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else if (source instanceof RefImpl) {
    getter = () => source.value
  } else {
    // 简化:假设为 reactive 对象,深度监听
    getter = () => traverse(source)
  }
  let oldValue
  const job = () => {
    const newValue = effect.run()
    cb(newValue, oldValue)
    oldValue = newValue
  }
  const effect = new ReactiveEffect(getter, job)
  if (options.immediate) {
    job()
  } else {
    oldValue = effect.run()
  }
}
// 深度遍历依赖
function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return value
  seen.add(value)
  for (const key in value) {
    traverse(value[key], seen)
  }
  return value
}

8.3 测试与验证

javascript

// 测试 reactive 和 effect
const state = reactive({ count: 0, nested: { a: 1 } })
let dummy
effect(() => {
  dummy = state.count * 2
})
console.log(dummy) // 0
state.count = 5
console.log(dummy) // 10

// 测试 computed
const countRef = ref(2)
const double = computed(() => countRef.value * 2)
console.log(double.value) // 4
countRef.value = 3
console.log(double.value) // 6(懒计算)

// 测试 watch
let oldVal, newVal
watch(() => state.count, (n, o) => {
  newVal = n; oldVal = o
})
state.count = 8
console.log(oldVal, newVal) // 5 8

与官方实现的差异:

  • 未处理数组 length 与迭代器相关依赖、Map/Set 的拦截。

  • 没有异步更新队列(同步执行),官方使用微任务批处理。

  • 未实现 effect 调度优先级、stop 时的完全清理。

  • 缺少边界情况处理(如代理只读对象、避免死循环等)。

完整可运行代码及在线示例请查看:CodeSandBox 链接(读者可自行创建运行环境测试)。


九、总结与思考

Vue 3 响应式系统基于 Proxy 重新设计,核心思想仍然是依赖收集触发更新,但实现上更加精简、高效。通过 WeakMap -> Map -> Set 三层结构管理依赖,通过懒代理提升初始化性能,通过 effect 统一管理副作用,通过 computed 和 watch 提供更上层的响应式工具。

本文核心回顾:

  • Proxy 对比 Object.defineProperty 的四大优势。

  • reactive / ref 的创建与依赖收集流程。

  • track / trigger 的源码实现与数据结构。

  • computed 的懒执行、缓存与 effect 的关系。

  • watch 的多源监听、深度追踪与回调调度。

优势与不足:

  • 优势:支持更多数据结构、性能更佳、代码更简洁、无 set/set/delete 等补丁 API。

  • 不足:不支持 IE,依赖 Proxy 导致 Polyfill 困难;深层嵌套大对象频繁操作时仍有优化空间。

展望: 未来响应式系统可能会引入更细粒度的调度、与编译时优化(如 Vue Vapor Mode)深度结合,甚至利用 TC39 新特性(如 Signals 提案)进一步统一状态管理。


本文通过对 Vue 3 响应式源码的逐层剖析,从设计思想到核心实现,再到手写 mini 系统,力求帮助读者建立起完整的知识图谱。欢迎在评论区分享你对响应式系统的理解或疑问,我们一起探讨!

更多推荐