《Vue3 从入门到大神33篇》Vue3 源码详解(三):响应式核心思想——从 Object.defineProperty 到 Proxy

本篇关键词:响应式本质、Proxy、依赖收集、effect、track、trigger

难度等级:⭐⭐⭐⭐☆(偏底层,但全程带 Demo)

工程价值:读懂这一篇,你能手写简易状态管理库、精准定位响应式 Bug、面试秒杀 90% 候选人。


一、开篇:为什么 Vue3 要抛弃 Object.defineProperty?

如果你用过 Vue2,一定写过这样的代码:

复制代码
data() {
  return {
    list: [1, 2, 3]
  }
}

然后你发现:

复制代码
this.list[0] = 99      // ❌ 不触发更新
this.list.length = 0   // ❌ 不触发更新
delete this.obj.a      // ❌ 不触发更新

Vue2 的解决方案是:set、delete、重写数组方法

这些不是"特性",而是补丁

Vue2 响应式的本质缺陷

Object.defineProperty只能做到:

  • ✅ 劫持已有属性

  • ❌ 无法监听新增 / 删除属性

  • ❌ 无法监听数组索引 / length

  • ❌ 需要递归遍历对象,性能差

👉 结论:Vue2 的响应式是"后天修补出来的"。


二、Vue3 的解法:Proxy 带来的降维打击

Vue3 使用 Proxy,一句话概括其优势:

Proxy 是对整个对象的拦截,而不是对属性的拦截。

Proxy 最直观的 Demo(先感性理解)

复制代码
const obj = { count: 0 }

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    console.log(`读取 ${key}`)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(`修改 ${key} = ${value}`)
    return Reflect.set(target, key, value, receiver)
  }
})

proxy.count           // 读取 count
proxy.count = 1       // 修改 count = 1

✅ 新增属性?能拦截

✅ 删除属性?能拦截

✅ 数组索引?能拦截

✅ 嵌套对象?懒代理(后面讲)


三、响应式系统的灵魂三问(源码级思维)

在阅读 Vue3 响应式源码前,必须先回答三个问题:

  1. 谁用了这个数据?(依赖收集)

  2. 数据变了通知谁?(依赖触发)

  3. 如何避免无限循环?(调度控制)

Vue3 的答案非常优雅:

概念 对应源码
依赖收集 track()
触发更新 trigger()
副作用函数 effect()

四、手写 Mini Reactive(VIP 核心环节)

不先写 Demo,直接读源码 = 劝退。

下面我们用 不到 30 行代码,实现一个最简响应式系统。

Step 1:存储依赖的数据结构

复制代码
// 存储所有副作用
const targetMap = new WeakMap()

// 当前正在执行的 effect
let activeEffect = null

设计说明:

  • WeakMap<target, Map<key, Set<effect>>>

  • 为什么用 WeakMap防止内存泄漏

  • 为什么用 Set防止重复收集


Step 2:track ------ 依赖收集

复制代码
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)
}

一句话解释

把"当前正在执行的副作用函数",存到"目标对象的指定属性"下。


Step 3:trigger ------ 触发更新

复制代码
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

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

一句话解释

找到"目标对象指定属性的所有副作用",挨个执行。


Step 4:effect ------ 副作用容器

复制代码
function effect(fn) {
  activeEffect = fn
  fn()                 // 执行一次,触发 track
  activeEffect = null
}

关键点:

  • 执行 fn()时,内部读取响应式数据

  • 触发 get→ 调用 track

  • 完成依赖收集


Step 5:reactive ------ 创建响应式对象

复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      track(target, key)
      return result
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    }
  })
}

Step 6:完整 Demo 跑起来 🎉

复制代码
const state = reactive({
  count: 0
})

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

state.count++   // 控制台自动打印:count = 1
state.count = 5 // 控制台自动打印:count = 5

恭喜你,你已经理解了 Vue3 响应式的 80% 核心逻辑。


五、对照 Vue3 源码:你写的就是简化版源码

你现在写的代码,本质上就是:

复制代码
packages/reactivity/src/
├── effect.ts     ← effect / track / trigger
├── reactive.ts   ← reactive()
└── baseHandlers.ts ← Proxy handlers

源码中的关键对应关系

手写 Vue3 源码
activeEffect effectStack
track() track()
trigger() trigger()
deps Dep类型

👉 区别只是:源码多了调度器、递归代理、边界处理。


六、工程视角:为什么这样设计?(VIP 重点)

1️⃣ 为什么用 WeakMap?

复制代码
WeakMap → key 必须是对象

好处:

  • 对象销毁 → 键值自动回收

  • 避免手动清理依赖(Vue2 的痛点)


2️⃣ 为什么 effect 要支持嵌套?

复制代码
effect(() => {
  effect(() => {
    console.log(state.count)
  })
})

Vue3 使用 effectStack​ 解决:

复制代码
effectStack.push(effect)
activeEffect = effect

3️⃣ 为什么需要调度器(Scheduler)?

试想:

复制代码
state.count++
state.count++

你不希望触发两次更新。

Vue3 通过:

  • queueJob

  • nextTick

  • flushPostFlushCbs

实现异步批量更新

(下一篇详细讲)


七、调试实战:在源码中验证你的理解

回到第二篇搭建的环境,在以下位置打断点:

复制代码
packages/reactivity/src/effect.ts

重点关注:

  • track()何时被调用

  • trigger()如何遍历 dep

  • activeEffect的变化过程

你会发现:

👉 源码逻辑和你手写的 Mini Reactive 几乎一致。


八、面试高频追问(收藏级)

  1. Proxy 一定能监听到所有变化吗?

    不能。MapSet需要单独处理(mutableHandlersvs collectionHandlers

  2. Vue3 为什么还要 ref?

    Proxy 无法代理基本类型,ref本质是 { value }对象的语法糖。

  3. track 和 trigger 一定是同步的吗?

    不一定。计算属性、watch、组件更新都可能被调度延迟。


九、本篇总结

✅ Vue3 响应式 = Proxy + WeakMap + Effect

✅ 核心流程:读取收集 → 修改触发

✅ 手写 Mini Reactive 是读源码的最短路径

✅ 工程中理解调度器、嵌套 effect、内存管理

一句话升华

Vue3 的响应式不是"魔法",而是一个设计极其精巧的观察者模式 + 代理模式组合。


十、下一篇预告

👉 《Vue3 源码详解(四):reactive 与 ref 源码解析,搞懂响应式对象创建过程》

相关推荐
Hyyy1 小时前
流式渲染的基础——SSE
前端·后端·面试
云空1 小时前
《Three.js 可操控3D魔方(键盘转动+打乱+复原)》
javascript·学习·游戏·3d·three.js
Csvn1 小时前
📐 Layout Thrashing——一个 `offsetHeight` 让你的页面卡成 PPT
前端
qq_529599382 小时前
react js循环滚动
前端·javascript·react.js
濮水大叔2 小时前
在 DTO 中使用 Form Layout 配置复杂表单,支持 Tabs/Groups/Sections
vue.js·typescript·node.js
索西引擎2 小时前
【React】严格模式:开发阶段质量保障的静态分析机制
前端·react.js·前端框架
张人玉2 小时前
Vue 3 + ECharts 智慧城市大屏——赣州智慧城市数据可视化平台
vue.js·echarts·智慧城市
玉宇夕落2 小时前
详细流式输出学习
前端
秃头披风侠_郑3 小时前
【uniapp】一文让你学会微信小程序+APP+H5全平台实战指南
前端·微信小程序·uni-app