【万字源码级剖析】深入理解 Vue 3 响应式系统:ref、reactive、computed 与 effect 的底层实现

摘要

本文从零手写一个 Mini Vue 响应式系统 ,逐步还原 reactiverefcomputedeffect 的核心逻辑,并结合 Vue 3.5 官方源码(@vue/reactivity)进行对照分析。你将彻底理解 Proxy 如何拦截数据、依赖如何收集(track)、更新如何触发(trigger),以及为什么 ref 需要 .value。全文包含 12 段可运行代码示例3 张原理图解5 个常见误区避坑指南 ,助你从"会用"进阶到"精通"。
关键词:Vue 3;响应式系统;Proxy;track;trigger;ref;reactive;computed;CSDN


一、引言:为什么你需要理解响应式原理?

很多开发者能熟练使用 refreactive,但遇到以下问题时却束手无策:

  • 为什么直接修改数组索引(arr[0] = 1)不触发更新?
  • 为什么解构 reactive 对象会失去响应性?
  • 为什么 ref 在模板中自动 .value,但在 JS 中必须手动写?
  • 为什么 computed 是懒执行且带缓存的?

根本原因:你只知其然,不知其所以然。

🎯 本文目标

通过 手写 Mini Vue + 源码对照 ,让你真正掌握 Vue 3 响应式系统的 设计哲学与实现细节


二、响应式系统的核心思想:依赖收集与派发更新

Vue 3 响应式基于 观察者模式 (Observer Pattern),但用 Proxy + WeakMap 实现了更高效的依赖管理。

核心流程三步走:

  1. Track(依赖收集):当组件读取某个响应式数据时,将其对应的更新函数(effect)记录下来;
  2. Trigger(派发更新):当数据被修改时,找出所有依赖它的 effect 并执行;
  3. Cleanup(依赖清理):避免内存泄漏,移除无效依赖。

🔁 关键数据结构

复制代码
// target -> key -> deps(Set<effect>)
const targetMap = new WeakMap<object, Map<string | symbol, Set<ReactiveEffect>>>()

三、手把手:从零实现 reactive

3.1 最简版 reactive(仅支持对象)

复制代码
// mini-vue/reactive.ts
type Target = Record<string, any>

// 存储依赖关系:target -> key -> effects
const targetMap = new WeakMap<Target, Map<string, Set<Function>>>()

function track(target: Target, key: string) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  // 当前正在执行的 effect
  if (activeEffect) {
    dep.add(activeEffect)
  }
}

function trigger(target: Target, key: string) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}

let activeEffect: Function | undefined = undefined

function effect(fn: Function) {
  activeEffect = fn
  fn() // 立即执行,触发 track
  activeEffect = undefined
}

export function reactive<T extends Target>(target: T): T {
  return new Proxy(target, {
    get(target, key: string, receiver) {
      const result = Reflect.get(target, key, receiver)
      // 收集依赖
      track(target, key)
      return result
    },
    set(target, key: string, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      // 派发更新
      trigger(target, key)
      return result
    }
  })
}

3.2 测试 reactive

复制代码
// test-reactive.ts
import { reactive, effect } from './mini-vue/reactive'

const state = reactive({ count: 0 })

effect(() => {
  console.log('count changed:', state.count)
})

state.count++ // 输出: count changed: 1

成功! 数据变化自动触发副作用函数。


四、升级:支持嵌套对象与数组

原版 reactive 会递归代理所有属性,我们来实现:

复制代码
// mini-vue/reactive.ts(增强版)
function createGetter() {
  return function get(target: Target, key: string, receiver: any) {
    const result = Reflect.get(target, key, receiver)
    
    // 递归处理嵌套对象
    if (typeof result === 'object' && result !== null) {
      return reactive(result)
    }
    
    track(target, key)
    return result
  }
}

function createSetter() {
  return function set(target: Target, key: string, value: any, receiver: any) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    
    // 区分新增属性 vs 修改属性
    const hadKey = Array.isArray(target) 
      ? Number(key) < target.length 
      : Object.prototype.hasOwnProperty.call(target, key)
    
    if (!hadKey) {
      // 新增属性:trigger ADD
      trigger(target, key)
    } else if (value !== oldValue) {
      // 修改属性:trigger SET
      trigger(target, key)
    }
    
    return result
  }
}

export function reactive<T extends Target>(target: T): T {
  // 避免重复代理
  if (isReactive(target)) return target
  
  return new Proxy(target, {
    get: createGetter(),
    set: createSetter()
  })
}

// 判断是否已是响应式对象
export function isReactive(value: unknown): boolean {
  return !!(value as any).__v_isReactive
}

// 在 reactive 中标记
export function reactive<T extends Target>(target: T): T {
  if (isReactive(target)) return target
  
  const proxy = new Proxy(target, {
    get: createGetter(),
    set: createSetter()
  })
  
  // 添加标记
  ;(proxy as any).__v_isReactive = true
  return proxy
}

💡 关键改进

  • 递归代理嵌套对象
  • 区分 ADD / SET 触发(对数组 length 变化至关重要)

五、实现 ref:包装基本类型

reactive 无法包装 numberstring 等基本类型,于是有了 ref

5.1 手写 ref

复制代码
// mini-vue/ref.ts
import { track, trigger } from './reactive'

class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true // 标记为 ref

  constructor(value: T) {
    this._value = value
  }

  get value() {
    track(this, 'value')
    return this._value
  }

  set value(newVal: T) {
    if (newVal !== this._value) {
      this._value = newVal
      trigger(this, 'value')
    }
  }
}

export function ref<T>(value: T) {
  return new RefImpl(value)
}

// 工具函数:判断是否为 ref
export function isRef(r: any): r is RefImpl<any> {
  return !!(r && r.__v_isRef === true)
}

// 自动解包 ref(用于模板)
export function unref(ref: any) {
  return isRef(ref) ? ref.value : ref
}

5.2 测试 ref

复制代码
// test-ref.ts
import { ref, effect } from './mini-vue'

const count = ref(0)

effect(() => {
  console.log('count:', count.value)
})

count.value++ // 输出: count: 1

为什么需要 .value

因为 ref 是一个对象,value 是其属性。JS 无法拦截基本类型赋值,只能通过对象属性 getter/setter 实现响应式。


六、实现 computed:懒执行 + 缓存

computed 本质是一个 带有缓存的 effect

6.1 手写 computed

复制代码
// mini-vue/computed.ts
import { effect, track, trigger } from './reactive'
import { isFunction } from '@vue/shared'

class ComputedRefImpl<T> {
  public readonly __v_isRef = true
  private _getter: () => T
  private _value: T
  private _dirty = true // 是否需要重新计算

  constructor(getter: () => T) {
    this._getter = getter
  }

  get value() {
    // 依赖收集
    track(this, 'value')
    
    if (this._dirty) {
      this._value = this._getter()
      this._dirty = false
    }
    return this._value
  }

  // 当依赖变化时,标记为 dirty
  notify() {
    this._dirty = true
    trigger(this, 'value')
  }
}

export function computed<T>(getter: () => T) {
  const runner = new ComputedRefImpl(getter)
  
  // 创建一个 effect,当依赖变化时通知 runner
  effect(() => {
    runner.notify()
  }, {
    lazy: true, // 不立即执行
    scheduler: () => {
      runner.notify()
    }
  })

  return runner
}

⚠️ 注意 :上述简化版未处理 effectscheduler,完整版需改造 effect 函数。

6.2 升级 effect 支持 scheduler

复制代码
// mini-vue/reactive.ts
type EffectOptions = {
  lazy?: boolean
  scheduler?: () => void
}

export function effect(fn: Function, options: EffectOptions = {}) {
  const _effect = () => {
    activeEffect = _effect
    fn()
    activeEffect = undefined
  }

  if (!options.lazy) {
    _effect()
  }

  // 保存 scheduler
  ;(_effect as any).scheduler = options.scheduler
  return _effect
}

// 修改 trigger
function trigger(target: Target, key: string) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effectFn => {
      if ((effectFn as any).scheduler) {
        (effectFn as any).scheduler()
      } else {
        effectFn()
      }
    })
  }
}

6.3 测试 computed

复制代码
// test-computed.ts
import { ref, computed } from './mini-vue'

const count = ref(1)
const double = computed(() => {
  console.log('计算 double...')
  return count.value * 2
})

console.log(double.value) // 输出: 计算 double... \n 2
console.log(double.value) // 输出: 2 (缓存生效!)

count.value = 2
console.log(double.value) // 输出: 计算 double... \n 4

验证成功:computed 懒执行、带缓存、依赖变化自动更新。


七、Vue 3 官方源码对照(@vue/reactivity)

我们来看看 Vue 3.5 的真实实现有何异同。

7.1 reactive 源码关键片段

复制代码
// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  // ...
  return createReactiveObject(
    target,
    false,
    mutableHandlers, // ← 核心 handler
    mutableCollectionHandlers,
    reactiveMap
  )
}

// mutableHandlers
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

🔍 对比我们的实现

  • 官方支持更多 trap(如 deletePropertyhas
  • 使用 ReactiveFlags 处理 isReactive 标记
  • 对数组、Map/Set 有特殊处理

7.2 ref 源码关键逻辑

复制代码
// packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
  return createRef(value, false)
}

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

💡 官方优化

  • 支持 shallowRef(浅层响应式)
  • ref(ref(x)) 做去重处理

八、三大核心 API 对比总结

特性 reactive ref computed
适用类型 对象/数组 任意类型 衍生值
访问方式 直接 obj.key ref.value computed.value
模板中 无需 .value 自动解包 自动解包
响应式原理 Proxy 代理整个对象 getter/setter 包装 带缓存的 effect
性能 高(批量代理) 中(单属性) 高(缓存)

最佳实践

  • reactive 定义对象状态
  • ref 定义基本类型或需跨组件传递的状态
  • computed 派生计算属性

九、5 大常见误区与避坑指南

❌ 误区 1:解构 reactive 对象会失去响应性

复制代码
// 错误!
const { count } = reactive({ count: 0 })
effect(() => {
  console.log(count) // 不会响应更新!
})

原因 :解构后 count 是普通 number,不再是 Proxy 属性。
正确做法

复制代码
// 方案1:不解构
const state = reactive({ count: 0 })
effect(() => console.log(state.count))

// 方案2:用 toRefs
import { toRefs } from 'vue'
const { count } = toRefs(reactive({ count: 0 }))

❌ 误区 2:直接替换整个 reactive 对象

复制代码
let state = reactive({ a: 1 })
state = reactive({ b: 2 }) // 原组件不会更新!

原因 :组件引用的是旧 Proxy 对象。
正确做法 :使用 Object.assign 或重置属性。

❌ 误区 3:在 computed 中执行副作用

复制代码
// 危险!
const badComputed = computed(() => {
  console.log('副作用') // 可能多次执行
  return someValue
})

原则:computed 应是纯函数,无副作用。

❌ 误区 4:忘记 ref 的 .value

复制代码
const count = ref(0)
setTimeout(() => {
  count = 1 // 类型错误!且失去响应性
}, 1000)

正确count.value = 1

❌ 误区 5:在非 effect 上下文中读取响应式数据

复制代码
const state = reactive({ count: 0 })
console.log(state.count) // 不会收集依赖!

只有在 effect(或 setup 中的模板)中读取才会 track


十、实战:用 Mini Vue 重构 TodoList

我们将用自己实现的响应式系统写一个简单 TodoList。

复制代码
// todo-app.ts
import { reactive, effect, ref, computed } from './mini-vue'

const todos = reactive([
  { id: 1, text: '学习 Vue 响应式', done: false }
])

const newTodoText = ref('')

const addTodo = () => {
  if (newTodoText.value.trim()) {
    todos.push({
      id: Date.now(),
      text: newTodoText.value,
      done: false
    })
    newTodoText.value = ''
  }
}

const completedCount = computed(() => {
  return todos.filter(t => t.done).length
})

// 渲染函数(模拟组件更新)
effect(() => {
  console.clear()
  console.log('=== 我的待办 ===')
  todos.forEach(todo => {
    console.log(`[${todo.done ? '✓' : ' '}] ${todo.text}`)
  })
  console.log(`已完成: ${completedCount.value}/${todos.length}`)
  console.log('输入新任务(回车添加):')
})

// 模拟用户输入
addTodo()

🎉 效果 :每次调用 addTodo(),控制台自动刷新列表!


十一、性能优化:避免不必要的 track/trigger

Vue 3 在以下场景做了优化:

  1. 相同值不 triggerset 时比较新旧值;
  2. 只读对象readonly 不触发 track;
  3. shallowReactive:不递归代理嵌套对象;
  4. WeakMap 自动 GC:target 被销毁,依赖自动清除。

💡 启示

在大型应用中,合理使用 shallowRefmarkRaw 可提升性能。


十二、结语:响应式不是魔法,而是精巧的设计

通过手写 Mini Vue,我们揭开了 Vue 3 响应式系统的神秘面纱:

  • Proxy 是基础,但不是全部;
  • WeakMap + Set 是灵魂,实现高效依赖管理;
  • effect 是桥梁,连接数据与视图;
  • ref/computed 是糖衣,让 API 更友好。

真正的高手,既能用好框架,也懂其底层逻辑

相关推荐
Mintopia21 小时前
“开源”和“闭源“,AI 模型的发展方向
前端·人工智能·aigc
Mintopia21 小时前
哈珀·李的《**杀死一只知更鸟**》(*To Kill a Mockingbird*)是一部关于**人性、正义与道德成长**的小说
前端
且菜且折腾21 小时前
react快捷键hook
javascript·react.js·ecmascript
什么都不会的Tristan21 小时前
Feed流(关注推送)
java·前端·数据库
IT_陈寒21 小时前
Vite 5.0 性能优化实战:从3秒到300ms的构建提速秘籍
前端·人工智能·后端
一路向前的月光21 小时前
前端采用electron-hiprint控件实现静默打印
前端·javascript·electron
Jagger_21 小时前
AI还原设计稿方法
前端
毛小茛21 小时前
pnpm 已经安装成功,但 npm 的全局 bin 目录没有进 PATH
前端·npm·node.js
胡琦博客21 小时前
基于华为开发者空间云开发环境(容器)探索前端智能化
前端·人工智能·华为云