从 Vue 2 到 Vue 3:响应式的演进

Vue 2 的响应式系统基于 Object.defineProperty,通过为对象的每个属性设置 getter 和 setter 来追踪依赖和触发更新。这种方案存在几个显著的局限性:无法检测属性的新增和删除、无法直接监听数组下标的变化、深层嵌套对象需要递归遍历导致性能开销较大。

Vue 3 全面转向了 ES6 的 Proxy,从根本上解决了上述问题。Proxy 可以拦截对象上的几乎所有操作,包括属性读写、属性删除、in 操作符、for...in 循环等,使得响应式覆盖面更加完整。

// Vue 2 的 defineProperty 方案(简化版)
function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      if (Dep.target) {
        dep.depend()
      }
      return val
    },
    set(newVal) {
      if (newVal === val) return
      val = newVal
      dep.notify()
    }
  })
}

// Vue 3 的 Proxy 方案(简化版)
function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key)
      return Reflect.get(obj, key, receiver)
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver)
      trigger(obj, key)
      return result
    },
    deleteProperty(obj, key) {
      const result = Reflect.deleteProperty(obj, key)
      trigger(obj, key)
      return result
    }
  })
}

Proxy 的核心机制

Proxy 对象接受两个参数:目标对象和处理器对象(handler)。处理器中可以定义各种拦截行为(trap),Vue 3 主要使用了 getsethasdeletePropertyownKeys 等 trap。配合 Reflect API 执行默认行为,保证了拦截逻辑与原始语义的一致性。

值得注意的是,Proxy 的拦截是惰性的。只有当嵌套对象被实际访问时,才会递归地对子对象进行 Proxy 包装,这比 Vue 2 在初始化时一次性递归遍历整个对象树要高效得多。

reactive() 与 ref()

Vue 3 提供了两种声明响应式状态的 API。reactive() 接受一个对象并返回其 Proxy 代理,适合管理复杂的对象状态。ref() 则可以接受任意类型的值,内部通过一个带有 __v_isRef 标记的对象进行包装,在模板中自动解包。

import { reactive, ref } from 'vue'

// reactive 适用于对象类型
const state = reactive({
  count: 0,
  user: { name: '张三', age: 25 }
})
state.count++ // 直接访问,触发响应

// ref 适用于基本类型或需要替换整个值的场景
const count = ref(0)
const list = ref([1, 2, 3])

count.value++   // JS 中需要 .value
list.value = [4, 5, 6] // 可以整体替换

// 模板中 ref 自动解包,无需 .value
// <template>
//   <p>{{ count }}</p>
// </template>

依赖收集与触发

Vue 3 的依赖收集系统基于 effect 函数。当组件的渲染函数执行时,会建立一个渲染 effect。在读取响应式属性时,get trap 中的 track() 函数会将当前正在执行的 effect 记录为该属性的依赖。当属性值发生变化时,set trap 中的 trigger() 函数会通知所有依赖重新执行。

// 简化的依赖收集实现
let activeEffect = null
const targetMap = new WeakMap()

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.scheduler ? effect.scheduler() : effect.run())
  }
}

// effect 执行
function effect(fn) {
  const _effect = {
    run() {
      activeEffect = _effect
      const result = fn()
      activeEffect = null
      return result
    }
  }
  _effect.run()
  return _effect
}

computed 的实现原理

computed 内部也是一个 effect,但它具有惰性求值和缓存机制。只有当依赖变化时才会标记为 dirty,在被访问时才重新计算。这种脏标记策略避免了不必要的重复计算。

function computed(getter) {
  let value
  let dirty = true

  const _effect = effect(getter, {
    scheduler() {
      dirty = true // 依赖变化时标记为脏
      trigger(obj, 'value') // 通知使用该 computed 的依赖
    }
  })

  const obj = {
    get value() {
      if (dirty) {
        value = _effect.run()
        dirty = false
      }
      track(obj, 'value')
      return value
    }
  }
  return obj
}

shallowReactive 与性能优化

shallowReactive() 只对对象的第一层属性进行响应式代理,嵌套对象不会被递归包装。在处理大型数据结构时,如果只有顶层属性需要响应式追踪,使用 shallowReactive 可以显著减少 Proxy 的创建数量和内存占用。

import { shallowReactive, reactive } from 'vue'

// shallowReactive:仅顶层响应
const shallow = shallowReactive({
  foo: 1,
  nested: { bar: 2 }
})
shallow.foo++     // 触发更新 ✓
shallow.nested.bar = 99 // 不触发更新 ✗

// reactive:深层响应
const deep = reactive({
  foo: 1,
  nested: { bar: 2 }
})
deep.nested.bar = 99 // 触发更新 ✓

类似地,shallowRef() 只追踪 .value 本身的变化,不会对值内部的属性做响应式转换。markRaw() 则可以标记一个对象永远不被转为响应式,适用于嵌入不可变第三方库实例的场景。合理选用这些 API,能在保持响应式能力的同时优化运行时性能。