《Vue3 从入门到大神25篇》手撕 Vue3 响应式原理 —— 手写 Mini-Vue

前言

前面的文章中,我们已经反复提到:

Vue3 的响应式基于 Proxy + Reflect

你也知道了:

  • reactive创建响应式对象

  • effect是副作用函数

  • track收集依赖

  • trigger触发更新

但问题是:

这些东西在源码里到底是怎么串起来的?

这一篇,我们从零手写一个 Mini-Vue 响应式系统,只用一个文件、不到 100 行代码,就把 Vue3 响应式的核心链路跑通。

写完之后你会发现:

原来 Vue3 的响应式并没有想象中那么神秘。


一、目标:我们要实现什么?

最终效果:

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

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

state.count++ // 控制台自动打印最新值

✅ 数据变化 → 自动重新执行副作用函数

这就是 Vue3 响应式的最小可用模型


二、核心设计思路

复制代码
┌─────────────────────────────────────────────┐
│                 响应式系统                    │
├─────────────────────────────────────────────┤
│                                             │
│  reactive(obj)  ──→  Proxy 代理对象          │
│       │                                     │
│       ▼                                     │
│  读取属性 (get)  ──→  track() 收集依赖        │
│       │                                     │
│       ▼                                     │
│  修改属性 (set)  ──→  trigger() 触发更新      │
│       │                                     │
│       ▼                                     │
│  effect() 重新执行                           │
│                                             │
└─────────────────────────────────────────────┘

三、数据结构设计

1️⃣ 依赖存储结构

复制代码
WeakMap (target → Map (key → Set (effects)))

// 存储所有依赖关系
const targetMap = new WeakMap()

为什么用这三层?

层级 类型 作用
WeakMap target → ... 每个响应式对象一个 Map
Map key → ... 每个属性一个 Set
Set effects 该属性的所有副作用函数

📌 WeakMap 的好处

当响应式对象被垃圾回收时,依赖关系自动清除,不会内存泄漏。


四、实现 track(依赖收集)

复制代码
// 当前正在执行的 effect
let activeEffect = null

function track(target, key) {
  if (!activeEffect) return

  // 获取 target 对应的 Map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 获取 key 对应的 Set
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  // 将当前 effect 加入 Set
  dep.add(activeEffect)
}

📌 核心逻辑

把"当前正在运行的副作用函数"和"被读取的属性"建立关联。


五、实现 trigger(触发更新)

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

  const effects = depsMap.get(key)
  if (!effects) return

  // 依次执行所有副作用函数
  effects.forEach(effect => effect())
}

📌 核心逻辑

找到和这个属性关联的所有副作用函数,全部重新执行。


六、实现 reactive(响应式对象)

复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)
      // 返回属性值
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      // 设置属性值
      const result = Reflect.set(target, key, value, receiver)
      // 触发更新
      trigger(target, key)
      return result
    }
  })
}

📌 这就是 Vue3 reactive 的最小实现

  • get 中收集依赖

  • set 中触发更新

  • Reflect 保证正确的 this 指向


七、实现 effect(副作用函数)

复制代码
function effect(fn) {
  // 包装成可调度函数
  const effectFn = () => {
    activeEffect = effectFn
    fn()
    activeEffect = null
  }

  // 立即执行一次
  effectFn()

  return effectFn
}

📌 关键点

  1. 把当前 effect 设为 activeEffect

  2. 执行用户传入的函数(此时会触发 get → track)

  3. 执行完毕后清空 activeEffect


八、完整代码(不到 60 行)

复制代码
// ===== Mini-Vue 响应式核心 =====

const targetMap = new WeakMap()
let activeEffect = null

function track(target, key) {
  if (!activeEffect) return
  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)
  }
  dep.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects?.forEach(fn => fn())
}

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

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn
    fn()
    activeEffect = null
  }
  effectFn()
  return effectFn
}

九、验证:跑一下试试

复制代码
const state = reactive({ count: 0, name: 'Vue3' })

effect(() => {
  console.log(`count is: ${state.count}`)
})

// 输出: count is: 0

state.count++

// 输出: count is: 1

state.count = 10

// 输出: count is: 10

完美运行!


十、对照 Vue3 源码

我们的实现 Vue3 源码
track() packages/reactivity/src/effect.ts中的 track
trigger() 同文件中的 trigger
reactive() packages/reactivity/src/reactive.ts
effect() packages/reactivity/src/effect.ts

📌 Vue3 源码更复杂 (处理了数组、Map/Set、嵌套对象等),但核心思想和我们写的一模一样


十一、局限性说明(诚实交代)

我们的 Mini-Vue 还不能处理:

场景 说明
嵌套对象 需要递归代理(Vue3 用 Reflect.get+ 懒代理)
数组 需要特殊处理 length和索引
Map / Set 需要重写方法
多次触发 需要 effect 栈防止嵌套
调度执行 需要 scheduler 控制执行时机

这些会在后续文章中逐步完善。


十二、面试高频问答

Q1:手写 Vue3 响应式的核心步骤?

  1. 用 WeakMap 存储依赖关系

  2. Proxy get 中 track 收集依赖

  3. Proxy set 中 trigger 触发更新

  4. effect 包装函数并立即执行

Q2:为什么用 WeakMap 而不是 Map?

WeakMap 的键是弱引用,不影响垃圾回收,防止内存泄漏。

Q3:Reflect 在这里的作用?

保证正确的 this 指向,统一操作 API。


十三、总结(源码级)

  • Vue3 响应式的本质是 Proxy + 依赖收集 + 触发更新

  • 核心数据结构是 WeakMap → Map → Set

  • track建立关联,trigger触发执行

  • 手写一遍,比看十遍源码都有用


📢 下期预告

👉 第 26 篇:Virtual DOM 与 Diff 算法 ------ Vue3 的高效更新机制

相关推荐
CappuccinoRose4 小时前
CSS、Less、Sass(含 SCSS)、Stylus
前端·css·less·sass·stylus
不听话坏13 小时前
Ignition篇(下 一) 动态执行前的事情
开发语言·前端·javascript
likeyi0713 小时前
require 和 import的区别
开发语言·前端
pany13 小时前
做 AI 友好的开源 Vue3 模板 🌈
前端·vue.js·ai编程
小二·14 小时前
React 19 + Next.js 15 现代前端开发实战:App Router / Server Components / 流式渲染
前端·javascript·react.js
daols8814 小时前
vue 甘特图 vxe-gantt 小时视图设置日期轴每间隔 3 个小时
vue.js·甘特图·vxe-gantt
谙忆102415 小时前
前端图片直传对象存储:OSS/S3 预签名 URL、STS 临时凭证与回调校验
前端
CHNE_TAO_EMSM16 小时前
Android studio 打开文件时自动下载源码
前端·javascript·android studio
一孤程17 小时前
Airtest自动化测试第五篇:小程序与Web测试——跨平台自动化全覆盖
前端·自动化测试·小程序·自动化·测试·airtest
IT_陈寒17 小时前
SpringBoot自动配置不是你以为的那样的智能
前端·人工智能·后端