Vue3响应式第一课:reactive/ref的底层逻辑

216 阅读5分钟

下面咱们尝试从零实现一个简易版 Vue3 响应式系统,通过五个关键模块搭建一个可运行的迷你响应式系统:

响应式系统架构图

image.png

1. 创建依赖管理中心

整体的流程如下:当访问响应式对象的属性时,会调用track函数来收集依赖。当属性被修改时,调用trigger函数触发所有相关的副作用函数effect

image.png

const targetMap = new WeakMap() // 存储所有响应式对象的依赖关系,形成 `target -> key -> effects` 的树形结构
let activeEffect = null         // 记录当前激活的副作用

// 依赖收集
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)  // 收集当前副作用
}

// 触发更新
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect()) // 触发所有关联副作用
  }
}

2. 实现 reactive 核心代理

使用Proxy来拦截对象的getset操作。

当访问响应式对象的属性时,会触发get陷阱,进行依赖收集;当修改对象属性时,触发set陷阱,检查值是否变化,然后触发更新。这样,当数据变化时,之前收集的effect会被执行,从而实现响应式更新。

// 函数接收一个对象obj,返回一个Proxy实例
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 oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      
      // 只有值变化时才触发更新
      if (!Object.is(oldValue, value)) {
        trigger(target, key)
      }
      return result
    }
  })
}

为什么用 Reflect.get/set

保持 this 指向正确性
使用 Reflect 方法确保在对象有继承关系时,receiver(即 Proxy 实例)能正确传递,避免 this 指向原始对象导致响应式丢失

// 示例:若不用 Reflect,可能导致问题
const parent = { a: 1 }
const child = reactive({ __proto__: parent })
child.a // 此时 receiver 应该是 child(Proxy),而非 parent

为什么用 Object.is

比 === 更严格,能正确处理 NaN 的特殊情况

Object.is(NaN, NaN)  // true
NaN === NaN          // false

3. 实现副作用注册机制

function effect(fn) {
  activeEffect = fn   // 挂载当前副作用
  fn()                // 触发依赖收集
  activeEffect = null // 清除激活状态,防止后续非effect触发的属性访问造成误收集
}

// 使用示例
const state = reactive({ count: 0 })

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

4. 实现 ref 的响应式包装

Vue3中ref的作用通常用于包装基本类型值(如数字、字符串等),使其成为响应式对象。因为Vue3的reactive使用Proxy,而Proxy只能代理对象,无法直接处理基本类型。所以ref通过创建一个带有value属性的对象来包裹原始值,从而实现对基本类型的响应式支持。

function ref(value) {
  return reactive({
    get value() {
      track(this, 'value')
      return value
    },
    set value(newVal) {
      if (!Object.is(value, newVal)) {
        value = newVal
        trigger(this, 'value')
      }
    }
  })
}

// 自动解包测试
const num = ref(42)
effect(() => {
  console.log('num is:', num.value) // 自动追踪.value访问
})

ref 与 reactive 的协作关系

特性refreactive
适用类型基本类型 + 对象仅对象
内存开销额外包装对象直接代理原对象
访问方式.value 访问直接属性访问
追踪粒度整个 value 属性每个独立属性
嵌套代理传入对象时二次代理自动浅层代理

性能优化技巧

// 避免重复代理对象
const obj = reactive({ data: null })
const data = ref(obj) // 内部会再次代理 obj → 非必要性能损耗

// 正确用法
const data = ref(null) // 基本类型无需担心
data.value = obj      // 赋值已代理对象

5. 嵌套对象处理

reactive函数使用Proxy来创建响应式对象。核心在于get拦截器里的递归处理。当访问对象属性时,如果属性的值是对象,就递归调用reactive,确保嵌套对象也是响应式的,优化如下:

function reactive(obj) {
  const observed = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      // 递归代理嵌套对象 ↓↓↓ 核心代码
      if (typeof res === 'object' && res !== null) {
        return reactive(res) // 自动深度代理
      }
      return res
    },
    // ...其他拦截器保持不变
  })
  return observed
}

// 测试嵌套响应
const state = reactive({ 
  user: { name: 'John' }
})

effect(() => {
  console.log('User name:', state.user.name) // 自动追踪嵌套属性
})

// 原始情况:
state.name.isProxyundefined // 嵌套对象未被代理

// 改进后:
state.name.isProxytrue // 自动深度代理

ps:实际Vue3源码使用WeakMap缓存代理对象

性能影响分析

操作类型无缓存处理有缓存处理
首次访问属性创建新代理对象创建并缓存代理对象
重复访问属性反复创建相同代理直接返回缓存代理
内存占用可能内存泄漏WeakMap自动回收
CPU消耗高频访问时消耗较大一次代理多次使用

特殊场景处理

  1. 循环引用问题
const obj = { a: null }
obj.a = obj // 创建循环引用
const proxyObj = reactive(obj)

// 无保护措施会导致无限递归 ↓
console.log(proxyObj.a.a.a) // 触发Maximum call stack size exceeded
  • 🛡️ 防御方案:代理前添加已处理对象检查
function reactive(obj) {
  if (obj.__isReactive) return obj // 标记已处理对象
  // ...代理逻辑
  observed.__isReactive = true
}
  1. DOM元素处理
reactive({ 
  el: document.getElementById('app') 
}) 
// 代理原生DOM对象可能导致意外行为
  • ⚠️ 最佳实践:Vue3源码会跳过对原生对象的代理

⚡ 核心原理验证测试

// 测试用例
const state = reactive({ count: 0 })
const arr = reactive([1, 2, 3])

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

effect(() => {
  console.log('Array length:', arr.length)
})

// 触发更新
state.count++        
arr.push(4) // 只能捕获直接属性修改,无法拦截 push 这类方法对 length 的隐式修改,留个思考:该如何优化?
arr[0] = 9 // 索引修改,这里无 effect 依赖该索引,不触发