引言:响应式编程的革命

Vue 3 的响应式系统彻底重写了 Vue 2 基于 Object.defineProperty 的实现,转而采用 ES6 的 Proxy 代理机制。这一变革不仅带来了性能的显著提升,更重要的是解决了 Vue 2 中诸多响应式限制。本文将深入剖析 Vue 3 响应式系统的核心机制,帮助你真正理解 ref() 和 reactive() 的底层原理。

一、Proxy:响应式系统的基石

1.1 为什么选择 Proxy?

// Vue 2 的响应式限制
const obj = { a: 1 }
Object.defineProperty(obj, 'a', {
  get() { /* 依赖收集 */ },
  set() { /* 触发更新 */ }
})

// 问题1:无法检测新增属性
obj.b = 2 // 非响应式

// 问题2:数组方法的hack实现
const arr = [1, 2, 3]
arr[3] = 4 // 非响应式
arr.length = 0 // 非响应式

// Vue 3 的 Proxy 解决方案
const proxy = new Proxy(obj, {
  get(target, key) {
    track(target, key) // 依赖收集
    return target[key]
  },
  set(target, key, value) {
    target[key] = value
    trigger(target, key) // 触发更新
    return true
  },
  deleteProperty(target, key) {
    delete target[key]
    trigger(target, key)
    return true
  }
})

// 所有操作都是响应式的
proxy.b = 2 // 响应式
proxy.c = 3 // 响应式
delete proxy.a // 响应式

1.2 Proxy 的陷阱方法详解

interface ProxyHandlers<T extends object> {
  get(target: T, key: string | symbol, receiver: any): any
  set(target: T, key: string | symbol, value: any, receiver: any): boolean
  has(target: T, key: string | symbol): boolean
  deleteProperty(target: T, key: string | symbol): boolean
  ownKeys(target: T): Array<string | symbol>
}

二、ref() vs reactive():底层实现深度对比

2.1 ref() 的内部结构

// ref 的核心实现
class RefImpl<T> {
  private _value: T
  private _rawValue: T // 原始值,用于比较
  public readonly __v_isRef = true
  
  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = value
    // 如果值是对象,用 reactive 包装
    this._value = isObject(value) ? reactive(value) : value
  }
  
  get value() {
    track(this, 'value') // 依赖收集
    return this._value
  }
  
  set value(newVal) {
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = isObject(newVal) ? reactive(newVal) : newVal
      trigger(this, 'value') // 触发更新
    }
  }
}

function ref<T>(value: T): Ref<T> {
  return new RefImpl(value)
}

2.2 reactive() 的内部结构

// reactive 的核心实现
const reactiveMap = new WeakMap() // 缓存代理对象

function reactive<T extends object>(target: T): T {
  // 已经是响应式对象则直接返回
  if (isReactive(target)) {
    return target
  }
  
  // 同一对象的代理缓存
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  
  const proxy = new Proxy(target, baseHandlers)
  reactiveMap.set(target, proxy)
  return proxy
}

const baseHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    if (key === '__v_isReactive') return true
    
    const res = Reflect.get(target, key, receiver)
    track(target, key) // 依赖收集
    
    // 深层响应式:如果值是对象,继续用 reactive 包装
    return isObject(res) ? reactive(res) : res
  },
  
  set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    
    if (hasChanged(value, oldValue)) {
      trigger(target, key) // 触发更新
    }
    
    return result
  }
}

2.3 关键差异对比表

特性 ref() reactive()
数据类型 任意类型 仅对象类型
访问方式 .value 直接属性访问
深层响应 自动深度转换 自动深度转换
模板解包 自动解包 直接使用
性能开销 较小(单一属性) 较大(整个对象)

三、依赖收集与触发更新:响应式核心机制

3.1 依赖收集(Track)原理

// 全局状态
let activeEffect: ReactiveEffect | undefined
const targetMap = new WeakMap() // 目标对象 → 键 → 依赖的映射

class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {}
  
  run() {
    activeEffect = this
    try {
      return this.fn()
    } finally {
      activeEffect = undefined
    }
  }
}

function track(target: object, key: unknown) {
  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)
}

3.2 触发更新(Trigger)原理

function trigger(target: object, key: unknown) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  const effects = depsMap.get(key)
  if (effects) {
    effects.forEach(effect => {
      if (effect !== activeEffect) {
        effect.run() // 重新执行副作用函数
      }
    })
  }
}

3.3 完整的响应式流程示例

// 1. 创建响应式数据
const state = reactive({ count: 0, name: 'Vue' })

// 2. 创建副作用(组件渲染就是副作用)
const effect = new ReactiveEffect(() => {
  console.log(`Count: ${state.count}, Name: ${state.name}`)
})

// 3. 首次执行,触发依赖收集
effect.run() // 输出: Count: 0, Name: Vue

// 4. 修改数据,触发更新
state.count = 1 // 输出: Count: 1, Name: Vue
state.name = 'Vue 3' // 输出: Count: 1, Name: Vue 3

四、响应性丢失问题与解决方案

4.1 直接赋值的响应性丢失

// 问题示例
const state = reactive({ user: { name: 'Alice', age: 25 } })

// 直接赋值导致响应性丢失
let { user } = state
user.name = 'Bob' // 响应式更新 ✓

// 重新赋值,失去响应性连接
user = { name: 'Charlie', age: 30 }
user.name = 'David' // 非响应式更新 ✗

4.2 解构 Props 的响应性丢失

<script setup>
// 父组件
const props = defineProps({
  user: {
    type: Object,
    required: true
  }
})

//  ❌ 错误:直接解构会失去响应性
const { name, age } = props.user

// ✅ 正确:使用 toRefs 保持响应性
const { name, age } = toRefs(props.user)

// ✅ 或者使用 computed
const userName = computed(() => props.user.name)
const userAge = computed(() => props.user.age)
</script>

4.3 toRef / toRefs 的实现原理

// toRef 实现
function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    __v_isRef: true,
    get value() {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  }
}

// toRefs 实现
function toRefs<T extends object>(object: T): ToRefs<T> {
  const ret: any = {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

五、最佳实践指南

5.1 数据结构与 API 选择矩阵

数据类型 推荐 API 示例 理由
基本类型 ref() const count = ref(0) 类型安全,模板自动解包
简单对象 reactive() const state = reactive({...}) 语法简洁,访问自然
复杂嵌套 reactive() + toRefs const { user, settings } = toRefs(state) 保持响应性,便于解构
组件 Props toRefs(props) const { id, data } = toRefs(props) 保持 Props 响应性
表单数据 reactive() const form = reactive({...}) 便于 v-model 绑定

5.2 组件状态管理策略

<script setup>
// 策略1:单一 reactive 对象(适用于相关状态)
const state = reactive({
  loading: false,
  data: null,
  error: null
})

// 策略2:多个 ref(适用于独立状态)
const count = ref(0)
const name = ref('')
const visible = ref(false)

// 策略3:组合式函数(逻辑复用)
function usePagination() {
  const page = ref(1)
  const pageSize = ref(10)
  const total = ref(0)
  
  return {
    page,
    pageSize,
    total
  }
}

const { page, pageSize } = usePagination()
</script>

5.3 性能优化建议

// 1. 避免不必要的响应式转换
const staticConfig = { api: '/data', timeout: 5000 } // ✅ 无需响应式
const dynamicState = reactive({ data: null, loading: false }) // ✅ 需要响应式

// 2. 使用 shallowRef/shallowReactive 优化性能
const largeList = shallowRef([]) //  ❌ 内部变化不触发更新
const nestedObj = shallowReactive({ deep: { data: null } }) //  ❌ 深层不响应

// 3. 合理使用 markRaw 标记非响应式对象
const heavyInstance = markRaw(new HeavyClass()) // 标记为原始对象
const state = reactive({ instance: heavyInstance }) // 不会转换为响应式

六、进阶:响应式系统的高级特性

6.1 自定义响应式 API

// 创建自定义 ref
function useDebouncedRef<T>(value: T, delay = 200) {
  let timeout: number
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

// 使用示例
const searchText = useDebouncedRef('', 300)

6.2 响应式工具函数深度解析

// isReactive / isRef 实现
function isReactive(value: unknown): boolean {
  return !!(value && (value as any).__v_isReactive)
}

function isRef<T>(r: Ref<T> | unknown): r is Ref<T> {
  return !!(r && (r as any).__v_isRef)
}

// unref 自动解包
function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? ref.value : ref
}

结语:掌握响应式思维

Vue 3 的响应式系统不仅仅是 API 的变更,更是一种编程范式的演进。理解其底层原理能够帮助我们:

  1. 写出更高效的代码:避免不必要的响应式开销
  2. 调试更精准的问题:快速定位响应性丢失的原因
  3. 设计更优雅的架构:合理运用响应式特性构建可维护应用

响应式编程的核心在于声明式地描述数据与UI的关系,让框架自动处理数据变化的传播。掌握这一思想,才能真正发挥 Vue 3 响应式系统的强大能力。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐