深入Vue3响应式原理:从Proxy到ref-reactive实现

本文将带你探讨 Proxy、ref/reactive、响应式、双向绑定之间的差别,然后一个一个认识它们,最后带你手写一个简化版的 Vue3 响应式系统,完整实现 reactiverefeffect、依赖追踪和更新触发。

本文默认你已经使用过 Vue3 中的 ref/reactive API。

一、引言

Vue 3 的响应式系统经过彻底重构,采用 ES6 的 Proxy 替代了 Vue 2 的 Object.defineProperty,这一变革带来了三大优势:

  1. 更强大的响应能力:支持动态属性增删、数组索引修改等场景
  2. 更高的性能:惰性监听和更精确的依赖追踪
  3. 更完善的数据结构支持:原生支持 Map、Set 等集合类型

理解这套机制不仅能提升开发效率,更是掌握 Vue 核心设计思想的关键。

二、核心概念关系

  • Proxy:ES6 中的代理,拦截对象的操作,用于实现响应式
  • 响应式:依赖收集(track)和更新触发(trigger),数据变化时更新相关数据
  • ref/reactive:暴露给开发者的响应式 API
  • 双向绑定:v-model

他们的关系是
Proxy 代理层 响应式系统 reactive/ref API 双向绑定 v-model

三、vue2 响应式的原理和缺陷

(了解 Object.defineProperty 可跳过本章节。)

在 vue2 中使用 Object.defineProperty 来实现响应式,可以拦截数据读取和赋值操作。

注意:下面代码实现一个最简单的数据劫持,仅仅是数据劫持。

javascript 复制代码
function defineReactive(obj, key) {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get() {
      console.log('读取:', key)
      return value
    },
    set(newVal) {
      console.log('更新:', key, newVal)
      value = newVal
    }
  })
}

// 对象
let obj = {
  name: 'zs',
  age: 18,
  isMale: true
}

// 遍历劫持
Object.keys(obj).forEach(key => defineReactive(obj, key))


if (obj.isMale) {
  console.log('是男性')
}

obj.name = 'ls'
const age = obj.age

// 执行结果:
// 读取: isMale
// 是男性
// 更新: name ls
// 读取: age

vue2 的这种实现方式会带来一些问题:

  • 不能监听对象属性的新增/删除
  • 数组 API 以及下标操作无法监听
  • 深层监听,造成性能问题

四、Proxy 的出现

(了解 Proxy 可跳过本章节。)

Vue3 中使用 Proxy 重构响应式原理,就可以解决上面的问题。

用法

Proxy(target, handler) 是一个构造函数,创建一个对象的代理,可以拦截对代理的基本操作。

  • target:要拦截的目标对象
  • handler:一个对象,定义了各种操作代理

什么是代理呢?可以简单理解为再操作这个代理之前设置一个拦截,当被访问、更新时,都要经过这层拦截,那么开发者就可以在这层拦截中进行各种各样的操作。

handler 可以拦截的操作有:getsethasdeletePropertyownKeysgetOwnPropertyDescriptordefinePropertypreventExtensionsgetPrototypeOfisExtensiblesetPrototypeOfapplyconstruct

演示

javascript 复制代码
// 目标对象
const obj = {
  name: 'zs',
  age: 18
}

// 代理目标对象的 get、set 操作
const p_obj = new Proxy(obj, {
  get(target, propKey) {
    console.log('读取:', propKey)
    // return target[propKey]
    return Reflect.get(target, propKey)

  },
  set(target, propKey, newVal) {
    console.log('更新:', propKey, newVal)
    // target[propKey] = newVal
    Reflect.set(target, propKey, newVal)
  }
})

// 操作代理对象的name
p_obj.name = 'ls'
const age = p_obj.age

// 查看目标对象的 name 属性
console.log('obj.name: ', obj.name)

// !!!新增属性!!!
obj.isMale = true

if (p_obj.isMale) {
  console.log('是男性')
}

// 执行结果
// 更新: name ls
// 读取: age
// obj.name:  ls
// 读取: isMale
// 是男性

可以看到,新增的 isMale 属性也是具有响应式的。

Reflect

通过上面的代码可以看到,我们不是直接操作 target 对象的,而是通过 Reflect API 去操作。

ES6 新推出的 Proxy API 的同时,同时也推出了 Reflect,基本上 Proxy 有的代理行为,Reflect 都有对应的静态方法。

至于为什么要使用 Relect ,有三点。

  1. 正确的 this 绑定
  2. 与 Proxy 方法的对称性
  3. 操作失败时的合理返回值

五、实现 ref/reactive

4.1 effect 和依赖收集

响应式的本质是"依赖追踪 + 变更通知"。当我们访问一个响应式数据时,Vue 会记录这个"依赖",当数据发生变化时,会通知相关依赖重新执行。这就引出了一个关键函数---- effect

javascript 复制代码
let activeEffect = null

function effect(fn) {
  activeEffect = fn
  fn() // 立即执行一次,触发依赖收集
  activeEffect = null // 执行完成后重置
}

当我们调用 effect(fn) 时,Vue 就记录下了正在执行的副作用函数,并在之后数据变动时重新执行它。

4.2 实现 reactive

使用 Proxy 来包裹对象,拦截它的 getset 操作。

javascript 复制代码
function reactive(target) {
  return new Proxy(target, {
    // 拦截属性读取
    get(target, key, receiver) {
      // 反射获取原始值
      const res = Reflect.get(target, key, receiver)
      track(target, key) // 收集依赖
      return res
    },

    // 拦截属性设置
    set(target, key, value, receiver) {
      // 反射设置值
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key) // 触发更新
      return result
    }
  })
}

访问数据时通过 track 函数收集依赖,当更新数据时通过 trigger 去一个个通知。

实现 ref

reactive 只能处理对象,而 ref 用于处理基本类型(如 numberstring)。它将基本类型包装为一个带 .value 的对象。

同样的,ref 也是跟 reactive 一样的思路:收集依赖、触发依赖。

reactive 不一样的是,

javascript 复制代码
function ref(value) {
  return {
    // ref 标识
    __is_ref: true,

    // value 的 getter
    get value() {
      // 收集依赖
      track(this, 'value')
      return value
    },

    // value 的 setter
    set value(newVal) {
      // 只有值变化时才触发更新
      if (value !== newVal) {
        value = newVal
        // 触发更新
        trigger(this, 'value')
      }
    }
  }
}

如何收集依赖、触发更新,track/trigger

为了追踪依赖并在变化时通知更新,我们使用 WeakMap → Map → Set 的结构:

javascript 复制代码
/**
 * 全局依赖存储
 * 结构: WeakMap<target, Map<key, Set<effect>>>
 */
// 第一级:目标对象 → 依赖映射
// 第二级:属性键 → 依赖集合
// 第三级:Set 存储 effect
const targetMap = new WeakMap()

function track(target, key) {
  // 没有活跃的 effect 则直接返回
  if (!activeEffect) return

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

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

  // 将当前 effect 添加到依赖集合
  dep.add(activeEffect)
}

function trigger(target, key) {
  // 获取 target 对应的所有依赖
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  // 获取 key 对应的所有 effect
  const dep = depsMap.get(key)
  if (dep) {
    // 执行所有关联的 effect
    dep.forEach(effect => effect())
  }
}

这种依赖收集的结构是响应式系统的核心,它允许我们精确地追踪某个 key 被哪些 effect 使用了。

六、总结

Vue3 响应式工作流
组件 Proxy拦截器 依赖系统 读取数据 (get) 触发get拦截 track(target, key) 存储activeEffect 到WeakMap→Map→Set 修改数据 (set) 触发set拦截 trigger(target, key) 通知关联effect执行 组件 Proxy拦截器 依赖系统

完整代码

javascript 复制代码
/**
 * 全局依赖存储
 * 结构: WeakMap<target, Map<key, Set<effect>>>
 */
// 第一级:目标对象 → 依赖映射
// 第二级:属性键 → 依赖集合
// 第三级:Set 存储 effect
const targetMap = new WeakMap()

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

/**
 * 注册副作用函数
 * @param {Function} fn - 需要响应式执行的函数
 */
function effect(fn) {
  // 设置当前活跃的 effect
  activeEffect = fn
  // 立即执行一次,触发依赖收集
  fn()
  // 执行完成后重置
  activeEffect = null
}

/**
 * 收集依赖
 * @param {Object} target - 目标对象
 * @param {string|symbol} key - 属性键
 */
function track(target, key) {
  // 没有活跃的 effect 则直接返回
  if (!activeEffect) return

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

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

  // 将当前 effect 添加到依赖集合
  dep.add(activeEffect)
}

/**
 * 触发更新
 * @param {Object} target - 目标对象
 * @param {string|symbol} key - 属性键
 */
function trigger(target, key) {
  // 获取 target 对应的所有依赖
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  // 获取 key 对应的所有 effect
  const dep = depsMap.get(key)
  if (dep) {
    // 执行所有关联的 effect
    dep.forEach(effect => effect())
  }
}

/**
 * 创建响应式对象
 * @param {Object} target - 目标对象
 * @returns {Proxy} 响应式代理
 */
function reactive(target) {
  return new Proxy(target, {
    // 拦截属性读取
    get(target, key, receiver) {
      // 反射获取原始值
      const res = Reflect.get(target, key, receiver)
      // 收集依赖
      track(target, key)
      return res
    },

    // 拦截属性设置
    set(target, key, value, receiver) {
      // 反射设置值
      const result = Reflect.set(target, key, value, receiver)
      // 触发更新
      trigger(target, key)
      return result
    }
  })
}

/**
 * 创建响应式引用
 * @param {*} value - 初始值
 * @returns {Object} 响应式引用对象
 */
function ref(value) {
  return {
    // ref 标识
    __is_ref: true,

    // value 的 getter
    get value() {
      // 收集依赖
      track(this, 'value')
      return value
    },

    // value 的 setter
    set value(newVal) {
      // 只有值变化时才触发更新
      if (value !== newVal) {
        value = newVal
        // 触发更新
        trigger(this, 'value')
      }
    }
  }
}

测试:

javascript 复制代码
// ===================== 测试案例 =====================

// 测试1: reactive 基本功能
console.log('===== reactive测试 =====')
const person = reactive({
  name: '张三',
  age: 25
})

effect(() => {
  console.log(`个人信息: ${person.name}, ${person.age}岁`)
})

person.name = '李四'  // 触发 effect
person.age = 30      // 触发 effect

// 测试2: ref 基本功能
console.log('\n===== ref测试 =====')
const count = ref(0)

effect(() => {
  console.log(`当前计数: ${count.value}`)
})

count.value++  // 触发 effect
count.value++  // 再次触发

// 测试3: ref 与 reactive 结合
console.log('\n===== 结合测试 =====')
const state = reactive({
  id: 1,
  score: ref(80)
})

effect(() => {
  console.log(`学生信息: ID=${state.id}, 分数=${state.score.value}`)
})

state.id = 2          // 触发 effect
state.score.value = 90 // 触发 effect

测试输出:

复制代码
===== reactive测试 =====
个人信息: 张三, 25岁
个人信息: 李四, 25岁
个人信息: 李四, 30岁

===== ref测试 =====
当前计数: 0
当前计数: 1
当前计数: 2

===== 结合测试 =====
学生信息: ID=1, 分数=80
学生信息: ID=2, 分数=80
学生信息: ID=2, 分数=90

参考

首发地址:https://blog.xchive.top/2025/deep-dive-into-vue3-reactivity-from-proxy-to-hand-rolling.html