深入理解 Vue 3 响应式系统
基于 Proxy 的响应式系统、依赖收集、触发更新、响应式 API 完整解析
什么是响应式系统?
定义:当数据发生变化时,自动更新依赖该数据的视图或计算属性的机制。
涉及场景:
- 数据驱动视图:修改数据自动更新 DOM
- 计算属性:依赖的数据变化时自动重新计算
- 侦听器:监听数据变化执行副作用
- 组件更新:props 或 state 变化触发重新渲染
作用:
- 简化开发:无需手动操作 DOM
- 提高性能:精确追踪依赖,按需更新
- 代码解耦:数据和视图分离
一、核心概念
1.1 响应式对象的创建
Vue 3 使用 ES6 Proxy 创建响应式对象,相比 Vue 2 的 Object.defineProperty 有以下优势:
Proxy 优势:
- 可以拦截对象的所有操作(13 种陷阱)
- 支持数组索引和 length 变化
- 支持 Map、Set、WeakMap、WeakSet
- 性能更好(懒代理)
简化实现:
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 用于包装基本类型值,使其具有响应式:
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 自动解包:
// 模板中自动解包
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 结构存储依赖关系:
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 函数
在访问响应式数据时收集依赖:
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 类
副作用函数的封装:
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 依赖收集流程
// 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 函数
当响应式数据变化时触发更新:
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 的执行时机:
// 组件更新使用队列调度
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 自动批量更新,多次修改只触发一次渲染:
const state = reactive({ count: 0, name: 'Vue' })
// 多次修改
state.count++
state.count++
state.name = 'Vue 3'
// 只会触发一次组件更新(在下一个微任务中)四、响应式 API 详解
4.1 reactive vs readonly vs shallowReactive
// 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++ // ❌ 非响应式实现原理:
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
// 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
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 也会变化实现原理:
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,具有缓存和懒计算特性:
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)
}缓存机制:
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) // 计算中... 24.5 watch vs watchEffect
// watch - 显式指定依赖
watch(
() => state.count,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{
immediate: true, // 立即执行
deep: true, // 深度监听
flush: 'post' // 在组件更新后执行
}
)
// watchEffect - 自动收集依赖
watchEffect(() => {
console.log(state.count) // 自动追踪
})实现原理:
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 对数组做了特殊处理:
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)
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 避免无限循环
// ❌ 会导致无限循环
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 倍):
// Vue 3.5 对大型深层数组做了特殊优化
const largeArray = reactive(
Array.from({ length: 10000 }, (_, i) => ({ id: i, value: i }))
)
// 修改单个元素性能大幅提升
largeArray[5000].value = 9996.2 最佳实践
1. 使用 shallowRef/shallowReactive 优化大对象:
// ❌ 深层响应式,性能开销大
const bigData = reactive({
items: Array.from({ length: 10000 }, (_, i) => ({ id: i }))
})
// ✅ 浅层响应式,只追踪根级别
const bigData = shallowReactive({
items: Array.from({ length: 10000 }, (_, i) => ({ id: i }))
})
// 整体替换时才触发更新
bigData.items = newItems2. 使用 markRaw 标记非响应式数据:
const state = reactive({
data: markRaw({
// 这个对象不会被代理
largeObject: { /* ... */ }
})
})3. 合理使用 computed 缓存:
// ❌ 每次都重新计算
const filtered = () => list.filter(item => item.active)
// ✅ 有缓存,依赖变化才重新计算
const filtered = computed(() => list.filter(item => item.active))面试高频题
1: Vue 3 响应式系统的核心原理是什么?
答案:
Vue 3 使用 ES6 Proxy 实现响应式系统,核心包括三个部分:
- 响应式对象创建:通过 Proxy 拦截对象的读写操作
- 依赖收集(track):在 get 陷阱中收集当前执行的 effect
- 触发更新(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)**实现缓存:
- 初始时
_dirty = true - 访问
computed.value时,如果_dirty为 true,执行计算并缓存结果 - 依赖变化时,调度器将
_dirty设为 true - 再次访问时重新计算
关键代码:
get value() {
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
return this._value
}5: watch 和 watchEffect 的区别?
答案:
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖声明 | 显式指定 | 自动收集 |
| 访问旧值 | ✅ 支持 | ❌ 不支持 |
| 懒执行 | 默认懒执行 | 立即执行 |
| 使用场景 | 需要旧值、异步操作 | 简单副作用 |
6: 如何避免响应式系统的性能问题?
答案:
- 使用 shallowRef/shallowReactive 优化大对象
- 使用 markRaw 标记非响应式数据
- 合理使用 computed 缓存计算结果
- 避免在模板中使用复杂表达式,改用 computed
- 大列表使用虚拟滚动,不要全部响应式化
- 使用 v-memo 缓存子树
7: Vue 3.5 响应式系统有哪些优化?
答案:
内存优化:减少 56% 内存使用
- 优化依赖存储结构
- 减少闭包创建
- 复用 effect 对象
大数组优化:性能提升 10 倍
- 特殊处理大型深层数组
- 优化数组方法的依赖收集
计算属性优化:解决悬挂 computed 的内存问题
8: 响应式系统如何处理数组?
答案:
Vue 3 通过 Proxy 可以直接拦截数组操作:
- 索引赋值:
arr[0] = 'new'✅ 响应式 - length 修改:
arr.length = 0✅ 响应式 - 数组方法:push、pop、splice 等 ✅ 响应式
特殊处理:
- 重写数组方法,暂停依赖收集避免无限循环
- length 变化时触发索引 >= newLength 的依赖
9: 如何理解依赖收集的双向记录?
答案:
双向记录:
- dep → effect:dep 记录依赖它的 effects
- effect → dep:effect 记录它依赖的 deps
作用:
- 清理依赖时可以快速找到所有相关的 dep
- 避免内存泄漏
- 支持 effect 的停止和重新收集
// effect 记录 deps
effect.deps.push(dep)
// dep 记录 effect
dep.add(effect)10: 响应式系统如何避免无限循环?
答案:
在 trigger 中检查当前执行的 effect:
effects.forEach(effect => {
// 避免在 effect 执行期间再次触发自己
if (effect !== activeEffect) {
effect.run()
}
})这样可以避免类似场景的无限循环:
watchEffect(() => {
state.count++ // 如果不检查,会无限触发自己
})