Vue.js: 响应式原理深挖

最后更新:2026-10-20

理解 Vue 3 响应式原理能让你写出更高效的代码、调试棘手的响应式问题。本课深入 Vue 3 响应式系统的底层实现:Proxy 拦截、依赖收集、触发更新,以及 ref / computed / watch 的内部机制。

Vue 3 用 ES6 Proxy 替代 Vue 2 的 Object.defineProperty,原生支持数组、Map、Set,性能提升 2x。理解原理后,你能避免 5 个常见响应式失效陷阱。

1. 你将学到


2. 一个 10 万行表格的"卡死"事件

(1) 痛点:100,000 项的 list,ref 包装后页面卡死

Alice 在后台加载了一个 10,000 行的商品表格:

JS
// ❌ 翻车版:deep reactive 处理 10 万行
const { ref } = Vue

const products = ref([
  { id: 1, name: 'Product 1', price: 99, ... },
  // ... 10,000 项 ...
])

// Vue 给每个属性、嵌套对象都创建 Proxy
// 内存占用:100MB+
// 首次渲染:3 秒
// 滚动:10 FPS(卡顿)

性能监控显示:

"10,000 行 × 4 个嵌套属性 × Proxy 开销 = 40,000 个 Proxy。每次属性访问触发 track。内存:100MB。FPS:10。"

(2) Vue shallowRef + markRaw 解法

JS
const { shallowRef, markRaw } = Vue

// ✅ shallowRef:只跟踪 .value 整体替换
const products = shallowRef([
  { id: 1, name: 'Product 1' },
  // ... 10,000 项(不会被 Proxy 包装)...
])

// ✅ markRaw:标记对象永远不响应(最快)
const staticConfig = markRaw({
  apiUrl: 'https://api.example.com',
  version: '1.0'
})

// 整体替换
products.value = await fetchProducts()  // 只触发 1 次更新

(3) 收益

优化后:


3. Proxy 拦截:Vue 3 响应式基石

(1) 什么是 Proxy?

Proxy 是 ES6 引入的对象包装器——可以拦截对象的 13 种操作(get / set / has / deleteProperty 等)。

JS
// 基础 Proxy 示例
const target = { name: 'Alice', age: 25 }

const proxy = new Proxy(target, {
  get(target, key) {
    console.log(`Read ${key}`)
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    console.log(`Write ${key} = ${value}`)
    return Reflect.set(target, key, value)
  }
})

proxy.name  // 输出 "Read name",返回 'Alice'
proxy.age = 26  // 输出 "Write age = 26"

(2) Vue 3 的 reactive 实现(简化版)

JS
// Vue 3 内部 reactivity 包的简化实现
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 1. 依赖收集:记录谁在用这个属性
      track(target, key)
      
      // 2. 递归包装:嵌套对象也变响应式
      const value = Reflect.get(target, key, receiver)
      if (isObject(value)) {
        return reactive(value)
      }
      return value
    },
    set(target, key, value, receiver) {
      // 1. 设置值
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 2. 触发更新:通知所有用这个属性的地方
      if (oldValue !== value) {
        trigger(target, key)
      }
      return result
    }
  })
}

(3) Proxy vs Object.defineProperty

维度 Object.defineProperty (Vue 2) Proxy (Vue 3)
数组支持 需特殊处理 原生支持
Map/Set ❌ ✅
添加新属性 需 Vue.set 自动响应
性能 一般 更好(懒代理)
浏览器支持 IE9+ IE 不支持(需 polyfill)

▶ 示例:响应式工具函数(isRef/isReactive/toRaw/triggerRef)

▶ 展开运行示例(70 行)
▶ 试一试

输出:

TEXT
三个面板自上而下:
1) count 是 ref? true、state 是 reactive? true、plain 是 ref? false;
2) state.count = 0,点「+1」后变 1;
3) users 第 0 项: Alice。
先点「改内部属性(不触发)」文字仍是 Alice,再点「triggerRef 手动触发」才变成 Bob。

4. track + trigger:依赖收集与触发更新

(1) 完整流程图

100%
sequenceDiagram
    participant T as Template
    participant C as Component
    participant R as Reactive<br/>(Proxy)
    participant W as WeakMap<br/>(target → Map)
    participant D as Data
    
    T->>C: 1. Render (读 count)
    C->>R: 2. get count
    R->>W: 3. track(target, 'count')
    W->>W: 4. 记录当前 activeEffect
    R->>D: 5. Read 0
    R-->>C: 6. Return 0
    C-->>T: 7. Display "Stock: 0"
    
    Note over D,T: 用户点击按钮
    
    D->>R: 8. set count = 1
    R->>W: 9. trigger(target, 'count')
    W->>W: 10. 找到所有依赖
    R->>C: 11. 调度更新
    C-->>T: 12. Display "Stock: 1"

(2) 全局依赖存储结构

JS
// Vue 3 内部数据结构
const targetMap = new WeakMap()
// targetMap = { obj1: Map1, obj2: Map2, ... }
// Map1 = { key1: Set1, key2: Set2, ... }
// Set1 = Set<activeEffect1, activeEffect2, ...>

function track(target, key) {
  // 1. 获取 target 对应的 Map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  
  // 2. 获取 key 对应的 Set
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }
  
  // 3. 记录当前激活的 effect
  if (activeEffect) {
    dep.add(activeEffect)
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const dep = depsMap.get(key)
  if (dep) {
    // 通知所有依赖重新执行
    dep.forEach(effect => effect.run())
  }
}

(3) 5 大核心要点

要点 说明
track 在 get 时 读属性 → 记录"谁在用"
trigger 在 set 时 改属性 → 通知"所有用的人"
WeakMap 存储 自动垃圾回收(target 销毁时自动清理)
activeEffect 当前正在执行的 effect(render / watch / computed)
批处理 多次修改只触发 1 次更新(nextTick)

5. ref 的内部实现

(1) ref 的源码(简化版)

JS
// Vue 3 ref 内部实现
class RefImpl {
  constructor(value) {
    this._value = isObject(value) ? reactive(value) : value
  }
  
  get value() {
    // 1. track
    track(this, 'value')
    return this._value
  }
  
  set value(newValue) {
    // 1. 比较新旧值
    if (hasChanged(this._value, newValue)) {
      this._value = isObject(newValue) ? reactive(newValue) : newValue
      // 2. trigger
      trigger(this, 'value')
    }
  }
}

function ref(value) {
  return new RefImpl(value)
}

(2) 自动解包原理

原理:模板编译时,编译器自动给 ref 加 .value(无需你手写)。


6. computed 的内部实现

(1) lazy + dirty 标志

JS
// Vue 3 computed 简化实现
function computed(getter) {
  const ref = {
    // 1. 缓存值
    _value: undefined,
    
    // 2. 是否过期(需要重算)
    _dirty: true,
    
    // 3. getter
    get value() {
      // 懒计算:只在第一次访问时算
      if (this._dirty) {
        this._value = effect(getter)  // 收集依赖
        this._dirty = false
      }
      
      // 4. track(自己被谁用)
      track(this, 'value')
      return this._value
    }
  }
  
  // 5. 当依赖变化时,标记 dirty
  effect(() => getter()).scheduled = () => {
    ref._dirty = true
    trigger(ref, 'value')  // 触发用 computed 的组件更新
  }
  
  return ref
}

(2) 缓存机制

JS
const count = ref(0)
const double = computed(() => count.value * 2)

console.log(double.value)  // 0(第一次计算)
console.log(double.value)  // 0(缓存,不重算)
count.value = 5
console.log(double.value)  // 10(重算)

7. 性能优化:shallowRef / shallowReactive / markRaw

(1) shallowRef:浅层响应

JS
const { shallowRef, triggerRef } = Vue

const products = shallowRef([
  { id: 1, name: 'iPhone', price: 999 },
  { id: 2, name: 'MacBook', price: 2499 }
])

// ❌ 不响应(修改 .value 内部属性)
products.value[0].name = 'iPad'
// 不会触发更新(shallowRef 只跟踪 .value 整体替换)

// ✅ 触发更新(整体替换)
products.value = [...products.value, { id: 3, name: 'iPad' }]

// ✅ 强制触发(修改内部后手动 trigger)
products.value[0].name = 'iPad'
triggerRef(products)

(2) shallowReactive:浅层 reactive

JS
const { shallowReactive } = Vue

const state = shallowReactive({
  user: { name: 'Alice' },
  settings: { theme: 'dark' }
})

// ✅ 响应:顶层属性变化
state.user = { name: 'Bob' }

// ❌ 不响应:嵌套属性
state.user.name = 'Charlie'

(3) markRaw:永久不响应

JS
const { markRaw, reactive } = Vue

// 第三方库实例(如 ECharts、Mapbox)不需要响应式
const map = markRaw(new Map())
const chart = markRaw(echarts.init(element))

const state = reactive({
  map,    // ✅ 不会被 Proxy 包装
  chart   // ✅ 性能提升 30%
})

(4) 5 大使用场景

场景 方案 性能提升
大数据列表(10万+) shallowRef 50x
第三方库实例 markRaw 30%
频繁创建/销毁对象 shallowReactive 20%
只读配置 markRaw 10%
不可变数据 shallowRef 5%

▶ 示例:5 大调试技巧(汇总)

JS
const { triggerRef, isRef, isReactive, isReadonly, isProxy, toRaw, markRaw } = Vue

// 1. watchEffect 打印依赖
watchEffect(() => console.log('依赖:', { a: state.a, b: state.b }))

// 2. 手动触发更新
triggerRef(stateRef)

// 3. 检查响应式状态
isReactive(obj)   // true/false
isRef(refObj)     // true/false

// 4. 原始对象(去除 Proxy)
const raw = toRaw(reactiveObj)

// 5. 性能标记
const fast = markRaw(expensiveObj)
▶ 试一试

输出:

TEXT
这段是速查代码,不渲染页面,结果都输出到控制台。
watchEffect 首次运行打印「依赖: { a: ..., b: ... }」,之后 state.a / state.b 变化时会再打印一次。
isReactive / isRef 返回 true 或 false;toRaw 得到不含 Proxy 的原始对象;markRaw 之后的对象不再被追踪。

8. 综合演练:手写响应式系统 + ref 内部结构

▶ 示例:track + trigger 简化版(手写 mini Vue)

▶ 试一试

输出:

TEXT
页面标题「手写 mini 响应式系统」,下面一行「state.count = 0」,再下面是按钮「+1(触发 track/trigger)」。
页面加载时控制台已打印一次「Render: 0」(activeEffect 首次执行,完成依赖收集)。
点击 +1 → state.count 变成 1,控制台打印「Render: 1」。

(1) 📊 5 大性能对比

模式 10万行性能 内存 适用
ref([]) 100ms 100MB 小数据
reactive({...}) 100ms 100MB 嵌套对象
shallowRef([]) 5ms 5MB 大列表
shallowReactive({...}) 30ms 30MB 浅对象
markRaw({...}) 2ms 2MB 第三方实例

▶ 示例:ref + reactive 内部结构

JS
const { ref, reactive, shallowRef, markRaw, isRef, isReactive, toRaw } = Vue

// ref:包装任何值
const count = ref(0)
console.log(isRef(count))   // true

// reactive:包装对象
const state = reactive({ count: 0 })
console.log(isReactive(state))   // true

// shallowRef:浅层
const list = shallowRef([1, 2, 3])

// markRaw:永久不响应
const map = markRaw(new Map())

// toRaw:去除 Proxy
const raw = toRaw(state)
console.log(raw === state)   // false
▶ 试一试

输出:

TEXT
控制台依次打印三行:
true    // isRef(count)
true    // isReactive(state)
false   // raw === state,toRaw 拿到的是不带 Proxy 的原始对象

(2) 📊 5 个常见失效根因

失效场景 根因 解决
整体替换 reactive 对象 reactive 引用变了 用 ref 包装或修改属性
解构丢失响应 解构出原始值 用 toRefs
数组直接赋值 index 内部属性被 Proxy 拦截 改用 splice 或整体替换
异步赋值不响应 异步函数脱离 track 用 ref/reactive 包装
嵌套对象属性不响应 Vue 3 已修复(Vue 2 需 Vue.set) 自动响应

❓ 常见问题

Q Vue 3 为什么用 Proxy 而不是 Object.defineProperty?
A Proxy 原生支持数组、Map、Set,能拦截更多操作(13 种 vs 2 种),添加新属性自动响应,性能更好(懒代理)。唯一缺点是不支持 IE11。
Q track 和 trigger 在哪里调用?
A track 在 reactive 对象的 get 方法中调用(get 时记录依赖)。trigger 在 set 方法中调用(set 时通知所有依赖)。
Q shallowRef 和 ref 的区别?
A ref 是深度响应(嵌套对象也是 Proxy)。shallowRef 只跟踪 .value 整体替换,不处理内部属性。适合大列表(10万+ 行)。
Q computed 怎么知道依赖变了?
A computed 内部用 effect 包装 getter。当 getter 中用到的响应式数据变化时,effect 触发,重置 dirty 标志,下次访问时重算。
Q watch 和 watchEffect 内部实现差异?
A watch 显式指定 source(依赖),回调拿到 newVal/oldVal。watchEffect 不指定 source,自动追踪函数体内的依赖,回调只拿新值。
Q shallowRef 修改内部属性如何触发更新?
A 用 triggerRef(shallowRef) 手动触发。场景:批量修改内部属性后强制刷新视图。
Q reactive 包装的 Proxy 在控制台显示什么?
A 显示 Proxy { ... } 包装对象,看不到原对象的属性。用 toRaw(proxyObj) 获取原始对象。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简化版的响应式系统(基于 Proxy):

    • 5 行 reactive 函数
    • 5 行 track + trigger
    • 演示:set 100 次 → 触发 effect 1 次(批处理)
  2. 进阶题(难度⭐⭐) 用 shallowRef + markRaw 优化大列表:

    • 1000 项的 products 列表(用 shallowRef)
    • 包含第三方库实例(用 markRaw)
    • 测量内存和 FPS 提升
  3. 挑战题(难度⭐⭐⭐) 实现一个完整的"依赖追踪调试器":

    1. watchEffect 打印所有依赖
    2. triggerRef 手动触发更新
    3. toRaw / isReactive / isRef 工具函数演示
    4. 5 种失效场景的根因分析(ref 替换 / 解构 / 数组 / 异步 / 嵌套)
    5. 性能基准:ref vs shallowRef vs markRaw 内存对比
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏