Proxy与Reflect详解(二):进阶篇——手写Vue3响应式系统

文章目录

Proxy与Reflect详解(二):进阶篇------手写Vue3响应式系统

概念概述

上一篇我们掌握了Proxy和Reflect的基础语法。这一篇我们深入Vue3源码,从零手写一个迷你响应式系统 ,理解reactive()ref()effect()computed()的底层原理。

Vue3响应式的核心只有四个字:收集依赖、触发更新

想象一个订阅系统:

  • track(收集依赖):谁用了这个数据,就登记到"订阅列表"里
  • trigger(触发更新):数据变了,通知所有订阅者重新执行

核心原理:依赖收集与触发更新

整体架构

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                     Vue3 响应式系统架构                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  reactive(obj)                                                  │
│      ↓                                                          │
│  Proxy代理 ──→ get拦截 ──→ track(target, key)   依赖收集         │
│      │          set拦截 ──→ trigger(target, key) 触发更新        │
│      ↓                                                          │
│  ┌───────────────────────────────────────────────────┐          │
│  │  targetMap (WeakMap)                               │          │
│  │  ┌─────────────────────────────────────────────┐  │          │
│  │  │  target → depsMap (Map)                      │  │          │
│  │  │  ┌────────────────────────────────────────┐  │  │          │
│  │  │  │  key → dep (Set<ReactiveEffect>)        │  │  │          │
│  │  │  └────────────────────────────────────────┘  │  │          │
│  │  └─────────────────────────────────────────────┘  │          │
│  └───────────────────────────────────────────────────┘          │
│                                                                 │
│  effect(fn)                                                     │
│      ↓                                                          │
│  ReactiveEffect实例                                              │
│      ↓                                                          │
│  执行fn → 触发get → track收集依赖 → 数据变化时重新执行fn          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

依赖存储结构(三级Map)

复制代码
targetMap: WeakMap<object, Map<string, Set<ReactiveEffect>>>

第1层:WeakMap  key=目标对象target,value=depsMap
第2层:Map      key=属性名,         value=dep(依赖集合)
第3层:Set      存储所有依赖这个属性的 ReactiveEffect 实例

示例:
targetMap = {
  { name: 'Alice', age: 25 } => {
    'name' => Set([effect1, effect2]),  // name被effect1和effect2使用
    'age'  => Set([effect1])            // age只被effect1使用
  }
}

完整可运行代码:手写迷你Vue3响应式系统

以下是完整可运行的迷你实现 ,约150行代码,覆盖reactiverefeffectcomputed核心功能:

javascript 复制代码
// ============================================================
//  第一部分:核心依赖管理
// ============================================================

// 全局变量:当前正在执行的effect(依赖收集的关键)
let activeEffect = null

// 依赖存储:WeakMap(target) → Map(key) → Set(effects)
const targetMap = new WeakMap()

// 收集依赖:记录"谁在读取哪个对象的哪个属性"
function track(target, key) {
  // 如果没有正在执行的effect,不需要收集
  if (!activeEffect) return

  // 第1层:取出target对应的depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  // 第2层:取出key对应的dep集合
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  // 第3层:将当前effect加入依赖集合
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    // 反向收集:effect知道自己依赖了哪些dep(用于清理)
    activeEffect.deps.push(dep)
  }
}

// 触发更新:通知所有依赖"数据变了"
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

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

  // 拷贝一份再遍历,防止在执行effect时修改dep导致无限循环
  const effectsToRun = new Set(dep)
  effectsToRun.forEach(effect => {
    // 避免effect触发自身导致无限递归
    if (effect !== activeEffect) {
      effect.run()
    }
  })
}

// ============================================================
//  第二部分:ReactiveEffect 类
// ============================================================

class ReactiveEffect {
  constructor(fn) {
    this.fn = fn              // 用户传入的副作用函数
    this.deps = []            // 反向依赖列表(用于清理)
  }

  run() {
    // 将自己设为当前活跃的effect
    const prevEffect = activeEffect
    activeEffect = this

    try {
      // 执行副作用函数,执行过程中会触发get → track收集依赖
      return this.fn()
    } finally {
      // 执行完毕后恢复上一个effect
      activeEffect = prevEffect
    }
  }

  stop() {
    // 清理所有依赖
    this.deps.forEach(dep => dep.delete(this))
    this.deps.length = 0
  }
}

// ============================================================
//  第三部分:reactive() ------ 创建响应式对象
// ============================================================

// 缓存:避免同一个对象被代理多次
const reactiveMap = new WeakMap()

// 用于判断是否是代理对象的标记
const RAW_KEY = Symbol('__raw__')
const IS_REACTIVE = Symbol('__isReactive__')

function reactive(target) {
  // 只能代理对象
  if (typeof target !== 'object' || target === null) {
    return target
  }

  // 已经是代理对象,直接返回
  if (target[IS_REACTIVE]) {
    return target
  }

  // 已经代理过,返回缓存
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target)
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 通过特殊key获取原始对象
      if (key === RAW_KEY) return target
      // 判断是否是reactive代理
      if (key === IS_REACTIVE) return true

      const result = Reflect.get(target, key, receiver)

      // 收集依赖
      track(target, key)

      // 深层响应式:访问到的嵌套对象也代理
      if (typeof result === 'object' && result !== null) {
        return reactive(result)
      }

      return result
    },

    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)

      // 值确实变了才触发更新
      if (oldValue !== value) {
        trigger(target, key)
      }

      return result
    },

    deleteProperty(target, key) {
      const hadKey = key in target
      const result = Reflect.deleteProperty(target, key)

      if (hadKey && result) {
        trigger(target, key)
      }

      return result
    }
  })

  reactiveMap.set(target, proxy)
  return proxy
}

// ============================================================
//  第四部分:ref() ------ 包装基本类型为响应式
// ============================================================

function ref(value) {
  // ref本质就是一个{ value: xxx }的reactive对象
  return reactive({ value })
}

// ============================================================
//  第五部分:effect() ------ 注册副作用函数
// ============================================================

function effect(fn) {
  const reactiveEffect = new ReactiveEffect(fn)

  // 立即执行一次,触发依赖收集
  reactiveEffect.run()

  // 返回runner函数,可以手动触发
  const runner = reactiveEffect.run.bind(reactiveEffect)
  runner.effect = reactiveEffect

  return runner
}

// ============================================================
//  第六部分:computed() ------ 计算属性(带缓存)
// ============================================================

function computed(getter) {
  // 缓存标记
  let dirty = true
  let cachedValue

  // 用effect监听getter的依赖变化
  const runner = effect(() => {
    // 依赖变化时,标记为脏
    dirty = true
    return getter()
  })

  // 返回一个只读的响应式对象
  return {
    get value() {
      if (dirty) {
        // 只有脏了才重新计算
        cachedValue = runner()
        dirty = false
      }
      return cachedValue
    }
  }
}

核心特性详解

特性1:依赖收集的时机

javascript 复制代码
// 依赖收集发生在effect执行时,触发get的那一刻

const state = reactive({ count: 0 })

effect(() => {
  // 执行到这里,读取state.count
  // → 触发Proxy的get拦截
  // → track(state, 'count')将当前effect加入dep
  console.log('count is:', state.count)
})

// 此时依赖关系:
// targetMap: {
//   state(原始对象) → {
//     'count' → Set([上面那个effect])
//   }
// }

特性2:依赖是动态的

javascript 复制代码
const state = reactive({
  ok: true,
  text: 'hello'
})

effect(() => {
  // 条件分支:只读取了ok
  console.log(state.ok ? state.text : 'world')
})

// 初始:effect同时依赖ok和text
// 如果把ok改为false,effect只依赖ok,不再依赖text
// → Vue3会在每次effect执行前清理旧依赖,重新收集

特性3:ref解包机制

javascript 复制代码
const count = ref(0)

// ref在reactive中自动解包
const state = reactive({ count })
console.log(state.count)    // 0(自动解包,不需要.value)
console.log(state.count.value) // undefined(没有这个属性)

完整使用示例

javascript 复制代码
// ============================================================
//  完整测试:验证手写的响应式系统
// ============================================================

console.log('===== 测试1:reactive基本响应 =====')
const state = reactive({ count: 0, name: 'Alice' })

effect(() => {
  console.log(`[effect1] count = ${state.count}`)
})
// 输出: [effect1] count = 0

state.count++
// 输出: [effect1] count = 1

state.count++
// 输出: [effect1] count = 2


console.log('\n===== 测试2:多个effect =====')
const user = reactive({ name: 'Alice', age: 25 })

effect(() => console.log(`[effect-A] name = ${user.name}`))
effect(() => console.log(`[effect-B] age = ${user.age}`))
// 输出: [effect-A] name = Alice
// 输出: [effect-B] age = 25

user.name = 'Bob'
// 输出: [effect-A] name = Bob

user.age = 30
// 输出: [effect-B] age = 30


console.log('\n===== 测试3:ref响应 =====')
const count = ref(0)

effect(() => {
  console.log(`[ref-effect] count = ${count.value}`)
})
// 输出: [ref-effect] count = 0

count.value++
// 输出: [ref-effect] count = 1


console.log('\n===== 测试4:computed计算属性 =====')
const price = ref(100)
const quantity = ref(3)

const total = computed(() => {
  console.log('[computed] 重新计算total')
  return price.value * quantity.value
})

console.log('total:', total.value)
// 输出: [computed] 重新计算total
// 输出: total: 300

console.log('total:', total.value)
// 输出: total: 300  (没有重新计算,使用缓存!)

quantity.value = 5
// 输出: [computed] 重新计算total(依赖变化,标记为脏)

console.log('total:', total.value)
// 输出: total: 500


console.log('\n===== 测试5:嵌套对象响应 =====')
const nested = reactive({
  user: {
    profile: {
      name: 'Alice'
    }
  }
})

effect(() => {
  console.log(`[nested] name = ${nested.user.profile.name}`)
})
// 输出: [nested] name = Alice

nested.user.profile.name = 'Bob'
// 输出: [nested] name = Bob


console.log('\n===== 测试6:数组响应 =====')
const list = reactive([1, 2, 3])

effect(() => {
  console.log(`[array] length = ${list.length}, items = ${list.join(',')}`)
})
// 输出: [array] length = 3, items = 1,2,3

list.push(4)
// 输出: [array] length = 4, items = 1,2,3,4

list[0] = 100
// 输出: [array] length = 4, items = 100,2,3,4


console.log('\n===== 测试7:delete响应 =====')
const config = reactive({ debug: true, verbose: false })

effect(() => {
  console.log(`[delete-test] debug exists: ${'debug' in config}`)
})
// 输出: [delete-test] debug exists: true

delete config.debug
// 输出: [delete-test] debug exists: false

开发坑点

坑点1:effect中的条件依赖导致更新遗漏

javascript 复制代码
const state = reactive({ a: 1, b: 2, flag: true })

effect(() => {
  if (state.flag) {
    console.log(state.a)  // flag为true时依赖a
  } else {
    console.log(state.b)  // flag为false时依赖b
  }
})

// 问题:flag为true时,effect只依赖flag和a
// 修改b不会触发更新(符合预期,因为b没被使用)
// 但如果我们把flag改为false,effect应该依赖b
// → Vue3的做法:每次effect执行前先清理所有旧依赖,重新收集

坑点2:effect嵌套的执行顺序

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

effect(() => {
  effect(() => {
    console.log('内层:', state.count)  // 内层effect先执行
  })
  console.log('外层:', state.count)    // 外层effect后执行
})

// Vue3用effect栈来处理嵌套:
// push effect → 执行内层 → push inner → 执行 → pop → 执行外层 → pop

坑点3:computed的缓存失效时机

javascript 复制代码
const state = reactive({ items: [1, 2, 3] })

const sum = computed(() => {
  // 如果computed内部有副作用,缓存可能导致副作用不执行
  console.log('计算中...')
  return state.items.reduce((a, b) => a + b, 0)
})

// 只读取value时才计算
console.log(sum.value)  // 计算中... 6

// 多次读取不会重新计算
console.log(sum.value)  // 6(没有"计算中...")
console.log(sum.value)  // 6(没有"计算中...")

// items变化后,下次读取才重新计算
state.items.push(4)
console.log(sum.value)  // 计算中... 10

工程实战场景

场景1:简易状态管理(替代Pinia的核心原理)

javascript 复制代码
// 基于reactive实现一个简易store
function createStore(options) {
  const { state, getters, actions } = options

  // 1. state用reactive包装
  const storeState = reactive(state())

  // 2. getters用computed包装
  const storeGetters = {}
  for (const key in getters) {
    storeGetters[key] = computed(() => getters[key](storeState))
  }

  // 3. actions绑定store上下文
  const storeActions = {}
  for (const key in actions) {
    storeActions[key] = (...args) => {
      actions[key].call(store, ...args)
    }
  }

  // 4. 组装store
  const store = {
    state: storeState,
    ...storeGetters,
    ...storeActions
  }

  return store
}

// 使用
const todoStore = createStore({
  state: () => ({
    todos: [],
    filter: 'all'
  }),

  getters: {
    activeTodos: (state) => state.todos.filter(t => !t.done),
    completedTodos: (state) => state.todos.filter(t => t.done),
    filteredTodos(state) {
      if (state.filter === 'active') return this.activeTodos
      if (state.filter === 'completed') return this.completedTodos
      return state.todos
    }
  },

  actions: {
    addTodo(text) {
      this.state.todos.push({ text, done: false, id: Date.now() })
    },
    toggleTodo(id) {
      const todo = this.state.todos.find(t => t.id === id)
      if (todo) todo.done = !todo.done
    }
  }
})

// 响应式使用
effect(() => {
  console.log('待办数量:', todoStore.state.todos.length)
})

todoStore.addTodo('学习Vue3响应式')  // 触发effect
todoStore.addTodo('写博客')          // 触发effect

场景2:表单双向绑定的底层原理

javascript 复制代码
// 模拟Vue的v-model底层原理
function modelBinding(target, key) {
  const state = reactive(target)

  // 双向绑定对象
  const binding = {
    // 读取时收集依赖
    get value() {
      track(state, key)
      return state[key]
    },
    // 设置时触发更新
    set value(newValue) {
      state[key] = newValue
    }
  }

  return binding
}

// 使用
const formData = modelBinding({ username: '' }, 'username')

effect(() => {
  console.log('当前输入:', formData.value)
})
// 输出: 当前输入:

formData.value = 'Alice'  // 触发更新
// 输出: 当前输入: Alice

formData.value = 'Bob'
// 输出: 当前输入: Bob

面试题总结

Q1:track和trigger的作用分别是什么?

核心答案

  • track(target, key):依赖收集。在effect执行过程中,当Proxy的get被触发时,将当前activeEffect记录到targetMap的对应位置
  • trigger(target, key):触发更新。当Proxy的set被触发时,从targetMap中取出该属性的所有effect并执行

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

核心答案:WeakMap的key是弱引用。如果target对象被垃圾回收,对应的依赖信息也会自动被回收,避免内存泄漏。如果用Map,即使target不再使用,其依赖信息仍会占用内存。

Q3:computed的缓存是如何实现的?

核心答案 :通过dirty标记控制。首次读取时执行getter并缓存结果,标记dirty=false。依赖变化时(effect中重新执行getter),将dirty标记为true。下次读取时发现dirty=true才重新计算。

Q4:为什么effect执行前要清理旧依赖?

核心答案:因为条件分支会导致依赖是动态的。如果上一次执行依赖了A和B,但这次只依赖A,那么B的dep中应该移除这个effect,否则B变化时会触发不必要的更新。


上一篇Proxy与Reflect详解(一):基础篇

下一篇Proxy与Reflect详解(三):面试实战篇

相关推荐
造火箭2 小时前
Android UI自动化测试可行性评估SKILL
android·功能测试·ui
love530love4 小时前
虚拟显示驱动导致 Photoshop / Camera Raw 卡死?OrayIddDriver 的锅
运维·人工智能·ui·photoshop·orayidddriver·hags 调度
RuoyiOffice4 小时前
抓包还能看到密码?Vue3 + Spring Boot 3 接口 AES/RSA 加解密(@ApiEncrypt)全链路实战
spring boot·vue3·aes·rsa·接口安全·spring boot 3·apiencrypt
名字还没想好☜6 小时前
React 19 useOptimistic 实战:点赞评论先更新 UI,失败自动回滚
前端·javascript·react.js·ui·react·useoptimistic
电子云与长程纠缠1 天前
UE5 Lyra PocketWorld进行3D内容UI预览 - 下
开发语言·学习·游戏·ui·ue5
RuoyiOffice2 天前
SpringBoot3+Vue3 接口访问日志实战:@ApiAccessLog 慢接口、参数脱敏与 OperateLog 如何分工
spring boot·vue3·traceid·spring boot 3·访问日志·apiaccesslog·操作日志
阿部多瑞 ABU2 天前
告别手工核图:基于 .NET + MuPDFCore 的 CAD 等轴测 PDF 材料表提取与新旧版本对比实战
后端·算法·ui·pdf·c#
tryCbest3 天前
Vue3项目在Nginx中配置代理后端IP
nginx·vue3