一、写在开头
前端框架的核心任务是将数据映射为 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.defineProperty Proxy
功能完整性 无法监听属性增删、数组索引和 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 的区别

对比维度 computed effect
执行时机 懒执行:只有被读取时才计算 立即执行一次,之后依赖变化时自动重新执行
返回值 返回一个 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 的区别

对比维度 watch computed
用途 执行副作用(异步请求、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 系统,力求帮助读者建立起完整的知识图谱。欢迎在评论区分享你对响应式系统的理解或疑问,我们一起探讨!

更多推荐