vue

响应式原理深度解析

By AI-Writer 12 min read

响应式原理深度解析

Vue 3 的响应式系统是其最核心的能力,也是区别于传统 DOM 操作的关键设计。理解响应式原理,不仅能帮助我们写出更高效的代码,还能在遇到问题时快速定位根因。本文将从 Proxy 基础出发,深入解析 Vue 3 响应式系统的全貌。

从 Object.defineProperty 到 Proxy

Vue 2 使用 Object.defineProperty 实现响应式,这一方案存在几个明显局限:无法监听新增属性、无法监听数组下标、需要递归遍历所有对象。Vue 3 采用了 Proxy 替代,从根本上解决了这些问题。

Proxy 的基本用法

javascript
const raw = { name: 'Alice', age: 28 }

// target: 代理目标对象
// key: 被访问的属性名
// value: 新设定的值
const handler = {
  get(target, key, receiver) {
    console.log(`读取属性: ${key}`)
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    console.log(`设置属性: ${key} = ${value}`)
    return Reflect.set(target, key, value, receiver)
  },
  deleteProperty(target, key) {
    console.log(`删除属性: ${key}`)
    return Reflect.deleteProperty(target, key)
  },
  has(target, key) {
    console.log(`in 运算符检查: ${key}`)
    return Reflect.has(target, key)
  }
}

const proxy = new Proxy(raw, handler)

proxy.name       // 触发 get → 输出: 读取属性: name
proxy.age = 30   // 触发 set → 输出: 设置属性: age = 30
delete proxy.age // 触发 deleteProperty → 输出: 删除属性: age
'name' in proxy  // 触发 has → 输出: in 运算符检查: name

核心概念:Proxy 会在对象访问的各个环节(读取、赋值、删除、遍历)自动触发预设的拦截函数,这正是 Vue 3 响应式系统的基础。

Proxy 的懒代理特性

与 Vue 2 递归遍历不同,Proxy 是惰性的——只在访问时才会创建深层代理:

javascript
const handler = {
  get(target, key, receiver) {
    const value = Reflect.get(target, key, receiver)
    // 仅当属性是对象时才递归代理
    if (value !== null && typeof value === 'object') {
      return new Proxy(value, handler)
    }
    return value
  }
}

const state = new Proxy({ user: { profile: { name: 'Bob' } } }, handler)

// user 层级才被代理,profile 在访问时才生成代理
console.log(state.user)         // Proxy 被创建
console.log(state.user.profile) // 深层 Proxy 被创建

Vue 3 响应式核心:reactive 与 ref

reactive 的实现

reactive 本质上是对普通对象包一层 Proxy:

typescript
// 简化版 reactive 实现(源码思路)
function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    console.warn('reactive 只接受对象类型')
    return target
  }

  const handler = {
    get(target, key, receiver) {
      // 依赖收集:在 get 时追踪当前激活的副作用
      track(target, key)
      const value = Reflect.get(target, key, receiver)
      // 对象类型递归代理
      if (isObject(value)) {
        return reactive(value)
      }
      return value
    },
    set(target, key, value, receiver) {
      const oldValue = target[key]
      const result = Reflect.set(target, key, value, receiver)
      // 只有值真正变化时才触发更新
      if (hasChanged(value, oldValue)) {
        // 触发依赖的副作用重新执行
        trigger(target, key, value, oldValue)
      }
      return result
    }
  }

  return new Proxy(target, handler)
}

// 使用示例
const state = reactive({
  count: 0,
  user: {
    name: 'Alice',
    tags: ['vue', 'typescript']
  }
})

state.count++          // 自动触发依赖更新
state.user.name = 'Bob' // 嵌套对象同样响应式

ref 的实现

ref 针对基本类型做了特殊处理——用包含 .value 属性的对象来存储值:

typescript
// 简化版 ref 实现
class RefImpl {
  private _value: any
  public dep: Set<Function>
  public __v_isRef = true

  constructor(value) {
    this._value = value
    this.dep = new Set()
  }

  get value() {
    // 读取时收集依赖
    trackRefValue(this)
    return this._value
  }

  set value(newVal) {
    // 新值与旧值不同时触发更新
    if (hasChanged(newVal, this._value)) {
      this._value = newVal
      // 触发所有依赖的副作用
      triggerRefValue(this)
    }
  }
}

function ref(value) {
  return new RefImpl(value)
}

// 自动解包:当 ref 作为 reactive 对象的属性时
// Vue 会自动访问其 .value,无需手动处理
const count = ref(0)
const state = reactive({ count })
console.log(state.count) // 自动解包为 0
state.count++           // 等价于 count.value++

关键设计:ref 通过 .value 保持了对基本类型的响应式能力,而 reactive 的属性如果是 ref,模板中会自动解包——这一设计让两者的使用体验高度统一。

依赖追踪:track 与 trigger

Vue 3 的响应式系统建立在”依赖收集 → 变化通知”两个阶段之上。

依赖收集(track)

当副作用函数读取响应式数据时,Vue 自动将这个函数记录到该数据的依赖列表中:

typescript
// 全局维护一个"当前活跃副作用"的引用
let activeEffect: Function | undefined

function track(target, key) {
  if (!activeEffect) return // 没有活跃副作用时跳过

  // targetMap 结构:target → key → Set<effect>
  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) // 建立 响应式数据 → 副作用 的映射
}

// effect 函数自动收集依赖
function effect(fn: () => void) {
  const wrappedFn = () => {
    activeEffect = wrappedFn
    fn() // 执行期间访问的响应式数据会被自动追踪
    activeEffect = undefined
  }
  wrappedFn()
}

// 使用示例
effect(() => {
  // 这里读取了 count,所以此 effect 依赖 count
  console.log(`计数:${state.count}`)
  // 这里读取了 name,所以也依赖 name
  console.log(`名称:${state.user.name}`)
})

变化通知(trigger)

当响应式数据被修改时,Vue 从依赖图中查找所有依赖它的副作用并触发执行:

typescript
function trigger(target, key, value, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

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

  // 依次执行所有依赖的副作用
  dep.forEach(effect => {
    // scheduler 用于控制更新时机(如 nextTick)
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  })
}

依赖图结构targetMap 维护了「响应式对象 → 属性名 → 副作用集合」的三层映射,使得任意数据变化都能精准找到需要重新执行的代码。

computed 的响应式实现

computed 本质上是一个带有缓存机制的响应式数据:

typescript
class ComputedRefImpl {
  private _value: any       // 缓存值
  private _dirty = true      // 缓存是否过期
  private effect: ReactiveEffect

  constructor(getter, private _setter) {
    // 创建一个带有 scheduler 的 effect
    this.effect = new ReactiveEffect(getter, () => {
      // 依赖变化时标记缓存过期,但不立即重新计算
      this._dirty = true
      // 通知依赖此 computed 的下游更新
      triggerRefValue(this)
    })
  }

  get value() {
    trackRefValue(this) // 收集当前活跃副作用对此 computed 的依赖
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run() // 懒计算,结果存入缓存
    }
    return this._value
  }
}

function computed(getter, options?) {
  return new ComputedRefImpl(getter, options?.set)
}

// 使用示例
const double = computed(() => state.count * 2)

// 第一次访问:执行 getter,结果被缓存
console.log(double.value) // 计算一次

state.count = 5
// count 变化后 _dirty 变为 true

// 第二次访问:检测到 _dirty,重新计算
console.log(double.value) // 重新计算,得到 10

缓存原理_dirty 标志位确保 getter 只在依赖真正变化时才重新执行,这是 computed 与普通 effect 的本质区别。

watch 与 watchEffect

watchwatchEffect 都用于响应副作用,但触发时机不同:

vue
<script setup>
import { ref, watch, watchEffect, reactive } from 'vue'

const count = ref(0)
const user = reactive({ name: 'Alice', age: 28 })

// watchEffect:立即执行,自动追踪依赖
watchEffect(() => {
  // 自动追踪 count 和 user.name
  console.log(`[watchEffect] ${user.name} 的计数:${count.value}`)
})

// watch:惰性执行,精确指定监听目标
watch(count, (newVal, oldVal) => {
  console.log(`[watch count] ${oldVal} → ${newVal}`)
}, { immediate: false })

// 监听多个数据源
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
  console.log(`[watch multiple] count: ${oldCount}→${newCount}, name: ${oldName}→${newName}`)
})

// deep 监听:深入对象内部
watch(() => ({ ...user }), (newVal, oldVal) => {
  console.log('user 对象深层变化')
}, { deep: true })

function increment() {
  count.value++
}
</script>

选择建议watchEffect 适合副作用逻辑简单、依赖明确的场景;watch 适合需要获取旧值、惰性触发、或精确控制触发时机的场景。

副作用清理

在组件卸载或监听器停止时,需要清理副作用防止内存泄漏:

typescript
// 在 effect 中返回清理函数
effect(() => {
  const timer = setInterval(() => {
    console.log(state.count)
  }, 1000)

  // 返回清理函数:组件卸载或重新运行时自动调用
  return () => clearInterval(timer)
})

// watch 的清理
const stop = watch(count, (newVal) => {
  if (newVal > 100) {
    console.log('达到上限,停止监听')
    stop() // 显式调用停止函数
  }
})

// onInvalidate(在 watchEffect 中)
watchEffect((onInvalidate) => {
  const source = axios.CancelToken.source()

  axios.get('/api/data', { cancelToken: source.token })
    .then(res => console.log(res))

  // 请求取消时清理
  onInvalidate(() => source.cancel('请求已取消'))
})

常见陷阱与规避

响应式丢失:解构与赋值

typescript
const state = reactive({ count: 0, name: 'Alice' })

// ❌ 解构后失去响应式
const { count, name } = state
count++ // 原 state.count 不变!

// ✅ 使用 toRefs 保持响应式
import { toRefs } from 'vue'
const { count, name } = toRefs(state)
count.value++ // 响应式正常工作

// ✅ 或使用 toRef 单独处理
import { toRef } from 'vue'
const count = toRef(state, 'count')

// ❌ 将响应式对象赋值给普通变量
const another = state
another.count = 99 // ❌ 丢失代理能力

// ✅ 使用 shallowReactive 创建浅层响应式
import { shallowReactive } from 'vue'
const shallow = shallowReactive({ deep: { value: 1 } })
shallow.deep.value = 2 // 不触发深层更新!

数组响应式的特殊处理

typescript
const list = reactive([1, 2, 3])

// ❌ 直接替换数组(丢失响应式)
list = [4, 5, 6] // 禁止!reactive 不允许重新赋值整个对象

// ✅ 使用数组方法(响应式)
list.push(4)      // 自动触发更新
list.splice(1, 1) // 删除第二个元素
list.length = 0   // 清空数组(触发更新)

// ✅ 替换整个数组的正确方式
Object.assign(list, [4, 5, 6]) // 原地替换,保留代理

// ✅ 使用 readonly 防止修改
import { readonly } from 'vue'
const safe = readonly(state) // 任何修改操作抛出警告

总结

Vue 3 的响应式系统以 Proxy 为基石,构建了”精准追踪 → 批量更新 → 自动解包”的完整链路:

  • Proxy 替代 Object.defineProperty,支持新增属性、数组下标、懒代理
  • reactive 包装普通对象为 Proxy,ref.value 包装基本类型
  • 依赖追踪:通过 targetMap 建立「数据 → 属性 → 副作用」的三层映射
  • computed_dirty 标志实现惰性求值与缓存
  • 常见陷阱:解构丢失响应式、赋值替换对象、数组方法误用

下一篇文章我们将学习 条件渲染与列表渲染,探讨 v-ifv-showv-for 的使用场景与性能差异。

#vue #vue3 #reactivity #响应式 #原理

评论

A

Written by

AI-Writer

Related Articles

vue
#10

Vitest 单元测试

使用 Vitest 和 Vue Test Utils 进行 Vue 3 组件单元测试,包括测试配置、组件测试、Composables 测试以及模拟异步数据等实战技巧

Read More
vue
#3

条件渲染与列表渲染

深入讲解 Vue 3 中 v-if、v-show、v-for 的使用场景与性能差异,以及 key 属性的作用和常见渲染陷阱的规避方法

Read More