Skip to content

深入理解 Vue 3 响应式系统

基于 Proxy 的响应式系统、依赖收集、触发更新、响应式 API 完整解析

什么是响应式系统?

定义:当数据发生变化时,自动更新依赖该数据的视图或计算属性的机制。

涉及场景

  • 数据驱动视图:修改数据自动更新 DOM
  • 计算属性:依赖的数据变化时自动重新计算
  • 侦听器:监听数据变化执行副作用
  • 组件更新:props 或 state 变化触发重新渲染

作用

  1. 简化开发:无需手动操作 DOM
  2. 提高性能:精确追踪依赖,按需更新
  3. 代码解耦:数据和视图分离

一、核心概念

1.1 响应式对象的创建

Vue 3 使用 ES6 Proxy 创建响应式对象,相比 Vue 2 的 Object.defineProperty 有以下优势:

Proxy 优势

  • 可以拦截对象的所有操作(13 种陷阱)
  • 支持数组索引和 length 变化
  • 支持 Map、Set、WeakMap、WeakSet
  • 性能更好(懒代理)

简化实现

typescript
function reactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 依赖收集
      track(target, key)
      
      const result = Reflect.get(target, key, receiver)
      
      // 如果是对象,递归代理(懒代理)
      if (isObject(result)) {
        return reactive(result)
      }
      
      return result
    },
    
    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
    },
    
    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      
      if (hadKey && result) {
        trigger(target, key)
      }
      
      return result
    }
  })
}

1.2 ref 的实现

ref 用于包装基本类型值,使其具有响应式:

typescript
class RefImpl<T> {
  private _value: T
  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T) {
    this._value = toReactive(value)
  }

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

  set value(newVal) {
    if (hasChanged(newVal, this._value)) {
      this._value = toReactive(newVal)
      // 触发更新
      triggerRefValue(this)
    }
  }
}

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

// 如果是对象,转为 reactive
function toReactive<T>(value: T): T {
  return isObject(value) ? reactive(value) : value
}

ref 自动解包

typescript
// 模板中自动解包
const count = ref(0)
// 模板中直接使用 {{ count }},不需要 .value

// reactive 中自动解包
const state = reactive({
  count: ref(0)
})
state.count // 0,自动解包
state.count = 1 // 自动更新 ref

二、依赖收集机制

2.1 数据结构

Vue 3 使用三层 Map 结构存储依赖关系:

typescript
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

// 结构示例:
// WeakMap {
//   target1: Map {
//     key1: Set [effect1, effect2],
//     key2: Set [effect3]
//   },
//   target2: Map {
//     key1: Set [effect4]
//   }
// }

为什么使用 WeakMap?

  • 弱引用:当对象不再被引用时,可以被垃圾回收
  • 避免内存泄漏:不会阻止对象被回收

2.2 track 函数

在访问响应式数据时收集依赖:

typescript
let activeEffect: ReactiveEffect | undefined

function track(target: object, key: unknown) {
  // 如果没有正在执行的 effect,直接返回
  if (!activeEffect) return
  
  // 获取 target 对应的 depsMap
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  
  // 获取 key 对应的 dep
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  
  // 将当前 effect 添加到 dep 中
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    // 双向记录:effect 也记录它依赖的 dep
    activeEffect.deps.push(dep)
  }
}

2.3 ReactiveEffect 类

副作用函数的封装:

typescript
class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  
  constructor(
    public fn: () => T,
    public scheduler?: EffectScheduler
  ) {}
  
  run() {
    // 如果已停止,直接执行函数
    if (!this.active) {
      return this.fn()
    }
    
    // 设置当前活跃的 effect
    const parent = activeEffect
    activeEffect = this
    
    try {
      // 清理旧依赖
      cleanupEffect(this)
      // 执行函数,触发依赖收集
      return this.fn()
    } finally {
      // 恢复之前的 activeEffect
      activeEffect = parent
    }
  }
  
  stop() {
    if (this.active) {
      cleanupEffect(this)
      this.active = false
    }
  }
}

// 清理 effect 的所有依赖
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

2.4 依赖收集流程

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

// 2. 创建 effect(如组件渲染函数)
const effect = new ReactiveEffect(() => {
  console.log(state.count) // 访问 count
})

// 3. 执行 effect
effect.run()
// ↓
// activeEffect = effect
// ↓
// 访问 state.count,触发 get 陷阱
// ↓
// track(state, 'count')
// ↓
// 将 effect 添加到 state.count 的依赖集合中

三、触发更新机制

3.1 trigger 函数

当响应式数据变化时触发更新:

typescript
function trigger(
  target: object,
  key: unknown,
  newValue?: unknown,
  oldValue?: unknown
) {
  // 获取 target 的 depsMap
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  
  // 收集需要执行的 effects
  const effects: Set<ReactiveEffect> = new Set()
  
  // 添加 key 对应的 effects
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      // 避免无限循环:如果 effect 正在执行,不重复添加
      if (effect !== activeEffect) {
        effects.add(effect)
      }
    })
  }
  
  // 如果是数组的 length 变化,需要触发索引 >= newLength 的依赖
  if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        dep.forEach(effect => effects.add(effect))
      }
    })
  }
  
  // 执行所有 effects
  effects.forEach(effect => {
    if (effect.scheduler) {
      // 如果有调度器,使用调度器执行
      effect.scheduler()
    } else {
      // 否则直接执行
      effect.run()
    }
  })
}

3.2 调度器(Scheduler)

调度器用于控制 effect 的执行时机:

typescript
// 组件更新使用队列调度
const queue: Set<ReactiveEffect> = new Set()
let isFlushing = false

function queueJob(job: ReactiveEffect) {
  queue.add(job)
  if (!isFlushing) {
    isFlushing = true
    // 在下一个微任务中执行
    Promise.resolve().then(flushJobs)
  }
}

function flushJobs() {
  queue.forEach(job => job.run())
  queue.clear()
  isFlushing = false
}

// 创建带调度器的 effect
const effect = new ReactiveEffect(
  () => console.log(state.count),
  () => queueJob(effect) // 调度器
)

3.3 批量更新

Vue 3 自动批量更新,多次修改只触发一次渲染:

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

// 多次修改
state.count++
state.count++
state.name = 'Vue 3'

// 只会触发一次组件更新(在下一个微任务中)

四、响应式 API 详解

4.1 reactive vs readonly vs shallowReactive

typescript
// reactive - 深层响应式
const state = reactive({
  nested: { count: 0 }
})
state.nested.count++ // ✅ 响应式

// readonly - 深层只读
const readonlyState = readonly(state)
readonlyState.nested.count++ // ❌ 警告:只读

// shallowReactive - 浅层响应式
const shallowState = shallowReactive({
  nested: { count: 0 }
})
shallowState.count = 1 // ✅ 响应式
shallowState.nested.count++ // ❌ 非响应式

实现原理

typescript
function shallowReactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key)
      // 不递归代理,直接返回原始值
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    }
  })
}

4.2 ref vs shallowRef vs triggerRef

typescript
// ref - 深层响应式
const obj = ref({ nested: { count: 0 } })
obj.value.nested.count++ // ✅ 响应式

// shallowRef - 浅层响应式
const shallowObj = shallowRef({ nested: { count: 0 } })
shallowObj.value = { nested: { count: 1 } } // ✅ 响应式
shallowObj.value.nested.count++ // ❌ 非响应式

// triggerRef - 手动触发更新
triggerRef(shallowObj) // 强制触发更新

使用场景

  • shallowRef:大对象优化,只关心整体替换
  • triggerRef:手动控制更新时机

4.3 toRef vs toRefs

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

// toRef - 单个属性
const count = toRef(state, 'count')
count.value++ // state.count 也会变化

// toRefs - 所有属性
const { count, name } = toRefs(state)
count.value++ // state.count 也会变化

实现原理

typescript
class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

  constructor(
    private readonly _object: T,
    private readonly _key: K
  ) {}

  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}

function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return new ObjectRefImpl(object, key)
}

4.4 computed

计算属性是特殊的 ref,具有缓存和懒计算特性:

typescript
class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true // 脏标记
  public readonly effect: ReactiveEffect<T>
  public readonly __v_isRef = true

  constructor(getter: () => T) {
    this.effect = new ReactiveEffect(getter, () => {
      // 依赖变化时,标记为脏
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // 触发计算属性的依赖
      }
    })
  }

  get value() {
    // 收集计算属性的依赖
    trackRefValue(this)
    
    // 只有脏了才重新计算
    if (this._dirty) {
      this._dirty = false
      this._value = this.effect.run()
    }
    
    return this._value
  }
}

function computed<T>(getter: () => T): ComputedRef<T> {
  return new ComputedRefImpl(getter)
}

缓存机制

typescript
const count = ref(0)
const double = computed(() => {
  console.log('计算中...')
  return count.value * 2
})

console.log(double.value) // 计算中... 0
console.log(double.value) // 0(使用缓存,不打印)

count.value = 1
console.log(double.value) // 计算中... 2

4.5 watch vs watchEffect

typescript
// watch - 显式指定依赖
watch(
  () => state.count,
  (newVal, oldVal) => {
    console.log(newVal, oldVal)
  },
  {
    immediate: true, // 立即执行
    deep: true, // 深度监听
    flush: 'post' // 在组件更新后执行
  }
)

// watchEffect - 自动收集依赖
watchEffect(() => {
  console.log(state.count) // 自动追踪
})

实现原理

typescript
function watchEffect(
  effect: () => void,
  options?: WatchEffectOptions
) {
  return doWatch(effect, null, options)
}

function watch<T>(
  source: () => T,
  cb: (newVal: T, oldVal: T) => void,
  options?: WatchOptions
) {
  return doWatch(source, cb, options)
}

function doWatch(
  source: any,
  cb: any,
  { immediate, deep, flush }: any = {}
) {
  let getter: () => any
  
  if (isRef(source)) {
    getter = () => source.value
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if (isFunction(source)) {
    getter = source
  }
  
  let oldValue: any
  
  const job = () => {
    const newValue = effect.run()
    if (cb) {
      cb(newValue, oldValue)
      oldValue = newValue
    }
  }
  
  const effect = new ReactiveEffect(getter, () => {
    if (flush === 'post') {
      queuePostFlushCb(job)
    } else {
      job()
    }
  })
  
  if (immediate) {
    job()
  } else {
    oldValue = effect.run()
  }
  
  return () => {
    effect.stop()
  }
}

五、特殊场景处理

5.1 数组响应式

Vue 3 对数组做了特殊处理:

typescript
const arrayInstrumentations: Record<string, Function> = {}

// 重写数组方法
;['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
  arrayInstrumentations[key] = function(...args: any[]) {
    // 暂停依赖收集
    pauseTracking()
    // 执行原始方法
    const res = Array.prototype[key].apply(this, args)
    // 恢复依赖收集
    resetTracking()
    return res
  }
})

// 在 Proxy get 中使用
function get(target: any, key: string | symbol, receiver: object) {
  if (isArray(target) && hasOwn(arrayInstrumentations, key)) {
    return Reflect.get(arrayInstrumentations, key, receiver)
  }
  
  track(target, key)
  return Reflect.get(target, key, receiver)
}

5.2 集合类型(Map、Set)

typescript
const mutableInstrumentations: Record<string, Function> = {
  get(key: any) {
    const target = toRaw(this)
    track(target, key)
    return target.get(key)
  },
  
  set(key: any, value: any) {
    const target = toRaw(this)
    const hadKey = target.has(key)
    const result = target.set(key, value)
    
    if (!hadKey) {
      trigger(target, key, 'add')
    } else {
      trigger(target, key, 'set')
    }
    
    return result
  },
  
  delete(key: any) {
    const target = toRaw(this)
    const hadKey = target.has(key)
    const result = target.delete(key)
    
    if (hadKey) {
      trigger(target, key, 'delete')
    }
    
    return result
  }
}

5.3 避免无限循环

typescript
// ❌ 会导致无限循环
const state = reactive({ count: 0 })

watchEffect(() => {
  state.count++
})

// ✅ 解决方案:在 trigger 中检查
function trigger(target: object, key: unknown) {
  // ...
  effects.forEach(effect => {
    // 避免在 effect 执行期间再次触发自己
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        effect.run()
      }
    }
  })
}

六、性能优化

6.1 Vue 3.5 响应式优化

内存优化(减少 56%):

  • 优化依赖存储结构
  • 减少闭包创建
  • 复用 effect 对象

大数组优化(性能提升 10 倍):

typescript
// Vue 3.5 对大型深层数组做了特殊优化
const largeArray = reactive(
  Array.from({ length: 10000 }, (_, i) => ({ id: i, value: i }))
)

// 修改单个元素性能大幅提升
largeArray[5000].value = 999

6.2 最佳实践

1. 使用 shallowRef/shallowReactive 优化大对象

typescript
// ❌ 深层响应式,性能开销大
const bigData = reactive({
  items: Array.from({ length: 10000 }, (_, i) => ({ id: i }))
})

// ✅ 浅层响应式,只追踪根级别
const bigData = shallowReactive({
  items: Array.from({ length: 10000 }, (_, i) => ({ id: i }))
})

// 整体替换时才触发更新
bigData.items = newItems

2. 使用 markRaw 标记非响应式数据

typescript
const state = reactive({
  data: markRaw({
    // 这个对象不会被代理
    largeObject: { /* ... */ }
  })
})

3. 合理使用 computed 缓存

typescript
// ❌ 每次都重新计算
const filtered = () => list.filter(item => item.active)

// ✅ 有缓存,依赖变化才重新计算
const filtered = computed(() => list.filter(item => item.active))

面试高频题

1: Vue 3 响应式系统的核心原理是什么?

答案

Vue 3 使用 ES6 Proxy 实现响应式系统,核心包括三个部分:

  1. 响应式对象创建:通过 Proxy 拦截对象的读写操作
  2. 依赖收集(track):在 get 陷阱中收集当前执行的 effect
  3. 触发更新(trigger):在 set 陷阱中触发依赖的 effect 执行

数据结构:WeakMap<target, Map<key, Set<effect>>>


2: 为什么使用 Proxy 而不是 Object.defineProperty?

答案

Proxy 优势

  • 可以拦截 13 种操作(get、set、deleteProperty、has 等)
  • 支持数组索引和 length 变化
  • 支持 Map、Set 等集合类型
  • 懒代理,性能更好
  • 可以代理整个对象,而不是逐个属性

Object.defineProperty 限制

  • 只能拦截属性的读写
  • 无法检测数组索引变化
  • 无法检测对象新增/删除属性
  • 需要递归遍历所有属性

3: ref 和 reactive 的区别和选择?

答案

reactive

  • 用于对象/数组
  • 返回 Proxy 对象
  • 解构会失去响应式
  • 不能替换整个对象

ref

  • 用于基本类型(也可用于对象)
  • 通过 .value 访问
  • 模板中自动解包
  • 可以替换整个值

选择建议

  • 基本类型用 ref
  • 对象优先用 reactive,需要替换时用 ref
  • 组合式函数返回值用 ref

4: computed 的缓存机制是如何实现的?

答案

computed 使用**脏标记(dirty flag)**实现缓存:

  1. 初始时 _dirty = true
  2. 访问 computed.value 时,如果 _dirty 为 true,执行计算并缓存结果
  3. 依赖变化时,调度器将 _dirty 设为 true
  4. 再次访问时重新计算

关键代码:

typescript
get value() {
  if (this._dirty) {
    this._dirty = false
    this._value = this.effect.run()
  }
  return this._value
}

5: watch 和 watchEffect 的区别?

答案

特性watchwatchEffect
依赖声明显式指定自动收集
访问旧值✅ 支持❌ 不支持
懒执行默认懒执行立即执行
使用场景需要旧值、异步操作简单副作用

6: 如何避免响应式系统的性能问题?

答案

  1. 使用 shallowRef/shallowReactive 优化大对象
  2. 使用 markRaw 标记非响应式数据
  3. 合理使用 computed 缓存计算结果
  4. 避免在模板中使用复杂表达式,改用 computed
  5. 大列表使用虚拟滚动,不要全部响应式化
  6. 使用 v-memo 缓存子树

7: Vue 3.5 响应式系统有哪些优化?

答案

  1. 内存优化:减少 56% 内存使用

    • 优化依赖存储结构
    • 减少闭包创建
    • 复用 effect 对象
  2. 大数组优化:性能提升 10 倍

    • 特殊处理大型深层数组
    • 优化数组方法的依赖收集
  3. 计算属性优化:解决悬挂 computed 的内存问题


8: 响应式系统如何处理数组?

答案

Vue 3 通过 Proxy 可以直接拦截数组操作:

  1. 索引赋值arr[0] = 'new' ✅ 响应式
  2. length 修改arr.length = 0 ✅ 响应式
  3. 数组方法:push、pop、splice 等 ✅ 响应式

特殊处理:

  • 重写数组方法,暂停依赖收集避免无限循环
  • length 变化时触发索引 >= newLength 的依赖

9: 如何理解依赖收集的双向记录?

答案

双向记录

  1. dep → effect:dep 记录依赖它的 effects
  2. effect → dep:effect 记录它依赖的 deps

作用

  • 清理依赖时可以快速找到所有相关的 dep
  • 避免内存泄漏
  • 支持 effect 的停止和重新收集
typescript
// effect 记录 deps
effect.deps.push(dep)

// dep 记录 effect
dep.add(effect)

10: 响应式系统如何避免无限循环?

答案

在 trigger 中检查当前执行的 effect:

typescript
effects.forEach(effect => {
  // 避免在 effect 执行期间再次触发自己
  if (effect !== activeEffect) {
    effect.run()
  }
})

这样可以避免类似场景的无限循环:

typescript
watchEffect(() => {
  state.count++ // 如果不检查,会无限触发自己
})