Skip to content

Vue 3 响应式原理

大白话解释: Vue 3 的响应式就像给对象"换了一个门卫"(Proxy)。所有对对象的操作(读取、修改、删除、检查)都要经过这个门卫,门卫会记录"谁来过",然后在数据变化时通知他们。

Proxy vs defineProperty 的区别:

  • defineProperty:只能在门口装监控,只能监控已有的属性,新增的看不到
  • Proxy:整个对象都被监控,任何操作都能拦截

为什么 Vue 3 要换用 Proxy?

  • 自动检测新增属性:不用再写 this.$set
  • 自动检测数组变化arr[0] = 'new' 能触发更新了
  • 性能更好:惰性代理,按需访问,不会一开始就递归所有属性

Vue 3 使用 Proxy 代理对象的读写操作,在读取时收集依赖(track),在修改时触发更新(trigger)。相比 Vue 2 的 Object.defineProperty,解决了新增属性、数组索引等检测问题。


Proxy vs Object.defineProperty

特性Vue 2 (defineProperty)Vue 3 (Proxy)
新增属性需要 $set自动检测
删除属性需要 $delete自动检测
数组索引无法检测自动检测
数组 length无法检测自动检测
性能初始化递归全部属性惰性代理,按需访问
兼容性IE9+不支持 IE
实现方式劫持属性的 getter/setter代理整个对象

Proxy 基础

ts
// 创建原始对象
const raw = { name: '张三', age: 25 }

// 创建 Proxy 代理
const proxy = new Proxy(raw, {
  // 拦截属性读取
  get(target, key, receiver) {
    console.log(`读取 ${String(key)}`)  // 记录读取操作
    return Reflect.get(target, key, receiver)  // 返回原始值
  },
  // 拦截属性设置
  set(target, key, value, receiver) {
    console.log(`设置 ${String(key)} = ${value}`)  // 记录设置操作
    const result = Reflect.set(target, key, value, receiver)  // 设置新值
    return result
  },
  // 拦截属性删除
  deleteProperty(target, key) {
    console.log(`删除 ${String(key)}`)  // 记录删除操作
    return Reflect.deleteProperty(target, key)  // 删除属性
  },
  // 拦截 in 操作符
  has(target, key) {
    console.log(`检查 ${String(key)} 是否存在`)  // 记录检查操作
    return Reflect.has(target, key)  // 返回检查结果
  },
})

// 测试代理
proxy.name       // 读取 name
proxy.age = 26   // 设置 age
delete proxy.age // 删除 age
'name' in proxy  // 检查 name

手写响应式系统

完整实现(简化版)

注意:以下实现是简化版,用于理解核心原理。Vue 3 实际实现更复杂,包含依赖清理、嵌套 effect、调度器等功能。

ts
// ===== 依赖管理 =====
type EffectFn = () => void  // 副作用函数类型

// 当前正在执行的副作用函数,用于依赖收集
let activeEffect: EffectFn | null = null

// 依赖收集桶:使用 WeakMap 存储对象到其属性依赖的映射
// 结构:target → key → effects(副作用函数集合)
const targetMap = new WeakMap<object, Map<string | symbol, Set<EffectFn>>>()

// 收集依赖:当读取属性时,将当前副作用函数收集到依赖中
function track(target: object, key: string | symbol) {
  if (!activeEffect) return  // 没有正在执行的副作用函数,无需收集

  // 获取或创建 target 的依赖 Map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 获取或创建属性的依赖集合
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }

  // 将当前副作用函数添加到依赖集合
  deps.add(activeEffect)
}

// 触发更新:当属性变化时,执行所有相关的副作用函数
function trigger(target: object, key: string | symbol) {
  const depsMap = targetMap.get(target)  // 获取 target 的依赖 Map
  if (!depsMap) return  // 没有依赖,直接返回

  const deps = depsMap.get(key)  // 获取属性的依赖集合
  if (deps) {
    deps.forEach((effect) => {
      // 避免无限循环:如果副作用函数是当前正在执行的,跳过
      if (effect !== activeEffect) {
        effect()  // 执行副作用函数
      }
    })
  }
}

// ===== reactive:将对象转换为响应式 =====
function reactive<T extends object>(target: T): T {
  return new Proxy(target, {
    // 拦截属性读取
    get(target, key, receiver) {
      track(target, key)  // 收集依赖
      const result = Reflect.get(target, key, receiver)  // 获取原始值
      // 惰性代理:读取到的嵌套对象也转为 reactive
      if (typeof result === 'object' && result !== null) {
        return reactive(result)  // 递归代理嵌套对象
      }
      return result
    },

    // 拦截属性设置
    set(target, key, value, receiver) {
      const oldValue = (target as any)[key]  // 获取旧值
      const result = Reflect.set(target, key, value, receiver)  // 设置新值
      // 值变化时才触发更新
      if (oldValue !== value) {
        trigger(target, key)  // 触发更新
      }
      return result
    },

    // 拦截属性删除
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)  // 删除属性
      trigger(target, key)  // 触发更新
      return result
    },
  })
}

// ===== ref:将值包装为响应式引用 =====
// 注意:这是简化版实现,Vue 3 实际使用 class 实现,支持更多特性
function ref<T>(value: T) {
  // 用闭包保存当前值,支持 .value 整体替换
  let inner = typeof value === 'object' && value !== null
    ? reactive(value as object)  // 对象类型自动转为 reactive
    : value  // 基本类型直接保存

  const refObj = {}  // 创建 ref 对象

  // 使用 defineProperty 定义 .value 属性
  Object.defineProperty(refObj, 'value', {
    get() {
      track(refObj, 'value')  // 收集依赖
      return inner  // 返回当前值
    },
    set(newValue) {
      if (newValue === inner) return  // 值未变化,跳过
      // 对象类型自动 reactive
      inner = typeof newValue === 'object' && newValue !== null
        ? reactive(newValue as object)  // 新对象转为 reactive
        : newValue  // 基本类型直接保存
      trigger(refObj, 'value')  // 触发更新
    },
  })

  return refObj  // 返回 ref 对象
}

// ===== effect(副作用函数)=====
// 执行副作用函数,并自动收集依赖
function effect(fn: EffectFn) {
  activeEffect = fn  // 设置当前正在执行的副作用函数
  fn()               // 执行函数,触发 getter 收集依赖
  activeEffect = null  // 清空当前副作用函数
}

// ===== 使用示例 =====
// 创建响应式对象
const state = reactive({ count: 0, name: '张三' })

// 注册副作用函数,自动收集依赖
effect(() => {
  console.log(`count is: ${state.count}`)  // 读取 state.count,触发依赖收集
})

// 修改响应式数据,触发更新
state.count = 1 // 输出: count is: 1
state.count = 2 // 输出: count is: 2

惰性代理(Lazy Proxy)

Vue 3 的 Proxy 是惰性的:只有在读取嵌套对象时才会将其转为 Proxy。

ts
// 创建嵌套对象
const state = reactive({
  user: {
    profile: {
      address: {
        city: '北京',
      },
    },
  },
})

// 此时只有 state 本身是 Proxy
// state.user 不是 Proxy,直到你读取它
console.log(state.user)  // 读取时,state.user 被转为 Proxy

// 现在 state.user 也是 Proxy 了
console.log(state.user.profile)  // 读取时,state.user.profile 被转为 Proxy

// 现在 state.user.profile 也是 Proxy 了

💡 这就是 Vue 3 性能更好的原因:不需要初始化时就递归代理所有属性。

注意:上面的简化实现中 get 每次读取都会递归调用 reactive,实际 Vue 3 源码会缓存已代理的对象,避免重复创建 Proxy。


shallowReactive / shallowRef

只代理第一层,不深度递归。

ts
import { shallowReactive, shallowRef } from 'vue'  // 导入浅层响应式 API

// shallowReactive —— 只有第一层是响应式的
const state = shallowReactive({
  name: '张三',       // ✅ 响应式:第一层属性
  nested: {
    count: 0,         // ❌ 不是响应式的:嵌套对象
  },
})

state.name = '李四'    // ✅ 触发更新:修改第一层属性
state.nested.count = 1 // ❌ 不触发更新:修改嵌套属性

// shallowRef —— 只有 .value 是响应式的
const data = shallowRef({ count: 0 })  // 创建浅层 ref
data.value = { count: 1 }   // ✅ 整体替换触发更新
data.value.count = 2        // ❌ 不触发更新:修改内部属性

💡 适合大型数据结构只关心引用变化的场景,如从接口返回的大列表。


readonly / shallowReadonly

ts
import { reactive, readonly, shallowReadonly } from 'vue'  // 导入只读 API

// 创建响应式对象
const original = reactive({ count: 0, user: { name: '张三' } })

// 深度只读 —— 所有层级都不能修改
const readonlyState = readonly(original)  // 创建深度只读代理
readonlyState.count = 1 // 警告!修改失败,控制台输出警告

// 浅层只读 —— 只有第一层不能修改
const shallowReadonlyState = shallowReadonly(original)  // 创建浅层只读代理
shallowReadonlyState.count = 1   // 警告!修改失败,第一层只读
shallowReadonlyState.user.name = '李四' // ✅ 可以修改,嵌套层可写

💡 provide/inject 中常用 readonly 防止后代篡改数据。


toRef / toRefs

toRef —— 将 reactive 对象的某个属性转为 ref

ts
import { reactive, toRef } from 'vue'  // 导入 reactive 和 toRef

// 创建响应式对象
const state = reactive({ count: 0, name: '张三' })

// toRef 创建一个与原始属性同步的 ref
const countRef = toRef(state, 'count')  // 将 state.count 转为 ref
countRef.value++           // state.count 也变为 1
state.count++              // countRef.value 也变为 2

toRefs —— 将 reactive 对象的所有属性转为 ref

ts
import { reactive, toRefs } from 'vue'  // 导入 reactive 和 toRefs

// 创建响应式对象
const state = reactive({ count: 0, name: '张三' })

// 解构时保持响应性:将每个属性转为 ref
const { count, name } = toRefs(state)
// count 是 Ref<number>,name 是 Ref<string>

count.value++ // state.count 也变了,保持同步

💡 Composables 中返回 reactive 对象时,常用 toRefs 让调用方可以安全解构。


toRaw / markRaw

ts
import { reactive, toRaw, markRaw } from 'vue'  // 导入 toRaw 和 markRaw

// toRaw —— 获取代理背后的原始对象
const state = reactive({ count: 0 })  // 创建响应式对象
const raw = toRaw(state)             // 获取原始对象,不是 Proxy
raw.count = 1                        // 不触发响应式更新

// markRaw —— 标记对象永远不被代理
const plain = markRaw({ nested: {} })  // 标记为非代理
const state2 = reactive({ data: plain })  // 包含非代理对象
console.log(state2.data === plain) // true,不是 Proxy

💡 markRaw 适合不需要响应式的大对象,如第三方库实例、大型配置对象。


响应式丢失的常见场景

1. 解构 reactive 对象

ts
const state = reactive({ count: 0, name: '张三' })  // 创建响应式对象

// ❌ 解构后丢失响应性
const { count, name } = state // count 和 name 是普通值,不是响应式

// ✅ 用 toRefs:保持响应性
const { count: countRef, name: nameRef } = toRefs(state) // countRef 和 nameRef 是 Ref 类型

2. 替换 reactive 对象

ts
let state = reactive({ count: 0 })  // 创建响应式对象
state = reactive({ count: 1 }) // ❌ 旧引用(如模板绑定)仍指向原对象,不会更新

// ✅ 修改属性:直接修改属性值
state.count = 1

// ✅ 或用 ref:ref 可以整体替换 .value
const stateRef = ref({ count: 0 })  // 用 ref 包装对象
stateRef.value = { count: 1 } // ✅ ref 可以替换 .value,保持响应性

3. 传递 reactive 属性给函数

ts
const state = reactive({ count: 0 })  // 创建响应式对象

// ❌ 传递的是值,不是引用
function increment(count: number) {
  count++ // 不会影响 state.count,只是局部变量
}

// ✅ 传递整个 reactive 对象
function increment(state: { count: number }) {
  state.count++  // 修改原始对象
}

// ✅ 或传递 ref:使用 toRef 创建 ref
const count = toRef(state, 'count')  // 将 state.count 转为 ref
function increment(count: Ref<number>) {
  count.value++  // 修改 ref,同步更新 state.count
}

4. 数组中的对象替换

ts
const list = reactive([{ id: 1, name: 'a' }])  // 创建响应式数组

// ✅ 直接修改属性:触发更新
list[0].name = 'b'  // 修改对象属性

// ✅ 整体替换数组项:触发更新
list[0] = { id: 1, name: 'b' }  // 替换整个对象

// ✅ push / splice:触发更新
list.push({ id: 2, name: 'c' })  // 添加新元素

调试响应式

ts
import { onRenderTracked, onRenderTriggered, ref, reactive } from 'vue'  // 导入调试钩子

// 追踪依赖收集:当组件渲染时收集依赖
// 注意:以下钩子仅在开发模式下有效,生产环境会被移除
onRenderTracked((event) => {
  console.log('跟踪:', event)  // 输出依赖收集事件
})

// 追踪触发更新:当响应式数据变化触发重新渲染时
onRenderTriggered((event) => {
  console.log('触发:', event)  // 输出触发更新事件
  // event 包含:target(原始对象)、key(变化的属性)、oldValue、newValue
  console.log('详细信息:', event.target, event.key, event.oldValue, event.newValue)
})

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准