Vue 3 Composition API:响应式系统与依赖追踪
标签:Vue,Composition API,响应式,依赖追踪,Proxy
前言:为什么需要深入理解响应式系统?
Vue 3 的 Composition API 不仅仅是一种新的代码组织方式,它建立在全新的响应式系统之上。这个系统使用 ES6 Proxy 替代了 Vue 2 的 Object.defineProperty,带来了更强大的依赖追踪能力和更好的性能表现。
本文将深入剖析:
- Vue 3 响应式系统的核心实现原理
- Proxy vs Object.defineProperty 的本质区别
- 依赖追踪的完整流程(含源码级分析)
- ref、reactive、computed 等API的内部机制
- 实战中的最佳实践与性能优化技巧
源码版本: Vue 3.4.31 (2024年稳定版本)
一、响应式系统概览:从数据变化到视图更新
Vue 3 的响应式系统可以概括为三个核心环节:
原始数据
Proxy代理层
依赖收集
track
副作用函数
effect
视图更新
触发更新
trigger
1.1 核心概念
- Reactive Target(响应式对象): 被Proxy包装的原始对象
- Effect(副作用函数): 依赖响应式数据的函数(如渲染函数、computed getter)
- Track(依赖收集): 在读取属性时,建立 property → effect 的映射
- Trigger(触发更新): 在修改属性时,重新执行所有相关的 effect
1.2 Vue 2 vs Vue 3 响应式方案对比
| 对比维度 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 拦截方式 | 只能拦截对象属性的 get/set | 拦截13种操作(get/set/has/delete等) |
| 数组监听 | 需要重写7个数组方法 | 原生支持,无需特殊处理 |
| 动态属性 | 无法检测新增属性 | 可以检测动态添加的属性 |
| Map/Set/WeakMap | 无法响应式处理 | 完整支持 |
| 性能开销 | 初始化时递归遍历所有属性 | 惰性代理,按需处理 |
| 内存占用 | 较高(每个属性都有依赖) | 较低(共享代理对象) |
二、Proxy 如何实现响应式?
2.1 基础示例
javascript
// Vue 3.4.x 源码位置:packages/reactivity/src/reactive.ts
const target = { count: 0 }
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 依赖收集:记录当前正在执行的effect
track(target, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// 只有值真正改变时才触发更新
if (oldValue !== value) {
trigger(target, key)
}
return result
}
})
2.2 Vue 3.4 的完整响应式处理器
源码文件: packages/reactivity/src/baseHandlers.ts
javascript
// Vue 3.4.31 简化版mutableHandlers
export const mutableHandlers: ProxyHandler<object> = {
get(target, key, receiver) {
// 1. 特殊处理内置Symbol(如__v_isReactive)
if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
// 2. 读取数组特殊属性时的处理(如length)
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 3. 依赖收集(核心步骤)
track(target, TrackOpTypes.GET, key)
// 4. 返回值处理(如果是对象,继续代理)
const res = Reflect.get(target, key, receiver)
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
},
set(target, key, value, receiver) {
// 1. 获取旧值用于比较
const oldValue = target[key]
// 2. 执行设置操作
const result = Reflect.set(target, key, value, receiver)
// 3. 只有在目标对象是自己时才触发(避免原型链上的误触发)
if (target === toRaw(receiver)) {
// 4. 判断是新增还是修改
const hadKey = hasOwn(target, key)
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (value !== oldValue) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
},
deleteProperty(target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, TriggerOpTypes.DELETE, key, undefined)
}
return result
},
// ... 其他拦截方法(has、ownKeys等)
}
三、依赖追踪机制:track与trigger的深度解析
3.1 依赖的数据结构
Vue 3 使用三层数据结构来存储依赖关系:
javascript
// 全局依赖关系存储
// targetMap = WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap()
// 示例数据结构:
targetMap = {
[target对象]: {
'count': Set[Effect1, Effect2],
'name': Set[Effect1]
}
}
targetMap WeakMap
target对象 Map
'count属性 Set'
'name属性 Set'
Effect渲染函数
Effect computed
3.2 track 函数源码分析
源码文件: packages/reactivity/src/effect.ts
javascript
// Vue 3.4.31 track函数核心逻辑
function track(target, type, key) {
// 1. 如果没有正在执行的effect,直接返回
if (!shouldTrack || activeEffect === undefined) {
return
}
// 2. 获取target对应的depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 3. 获取key对应的dep集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 4. 将当前effect添加到dep中(利用Set去重)
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
// 5. 双向绑定:effect也需要记录自己的依赖
activeEffect.deps.push(dep)
}
}
关键点解析:
activeEffect:全局变量,指向当前正在执行的副作用函数shouldTrack:控制是否进行依赖收集(如computed内部访问时不重复收集)- 双向绑定结构:effect 记录 deps 数组,用于 cleanup 时快速查找
3.3 trigger 函数源码分析
javascript
// Vue 3.4.31 trigger函数核心逻辑
function trigger(target, type, key, newValue, oldValue) {
// 1. 获取依赖关系
const depsMap = targetMap.get(target)
if (!depsMap) return
// 2. 收集需要执行的effects
const effects = new Set<ReactiveEffect>()
// 3. 添加直接依赖的effects
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effects.add(effect))
}
// 4. 特殊处理:数组长度变化、迭代操作等
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
// 触发迭代器相关的effects(如for...of循环)
const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
depsMap.get(iterationKey)?.forEach(effect => effects.add(effect))
}
// 5. 执行effects(利用Set避免重复)
effects.forEach(effect => {
// 避免无限递归(effect内部修改导致自己再次执行)
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler() // 计算属性使用scheduler延迟执行
} else {
effect.run() // 普通effect直接执行
}
}
})
}
3.4 依赖收集完整流程图
targetMap activeEffect track() Proxy拦截器 用户代码 targetMap activeEffect track() Proxy拦截器 用户代码 建立依赖关系: state.count → 渲染函数 访问 state.count 调用track(target, 'count') 读取activeEffect 返回当前渲染函数 targetMap.get(target).get('count') 返回Set<Effect> effect添加到Set
四、核心API实现原理
4.1 reactive:创建响应式对象
源码文件: packages/reactivity/src/reactive.ts
javascript
// Vue 3.4.31 reactive函数核心逻辑
function reactive<T extends object>(target: T): UnwrapNestedRefs<T> {
// 1. 只处理对象类型
if (!isObject(target)) {
console.warn(`value cannot be made reactive: ${String(target)}`)
return target
}
// 2. 如果已经是响应式对象,直接返回
if (target[ReactiveFlags.IS_REACTIVE]) {
return target
}
// 3. 创建Proxy对象
return createReactiveObject(
target,
false, // isReadonly
mutableHandlers, // get/set处理器
mutableCollectionHandlers // 集合类型处理器
)
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
// 4. 从缓存中查找(避免重复代理)
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 5. 创建新Proxy
const proxy = new Proxy(
target,
isCollectionType(target) ? collectionHandlers : baseHandlers
)
// 6. 缓存代理对象
proxyMap.set(target, proxy)
return proxy
}
4.2 ref:响应式值的包装器
javascript
// Vue 3.4.31 ref类定义
class RefImpl {
private _value: T
public dep?: Set<ReactiveEffect> = undefined
public readonly __v_isRef = true
constructor(value, public readonly __v_isShallow: boolean) {
this._value = __v_isShallow ? value : toReactive(value)
this.dep = undefined // 依赖集合
}
get value() {
// 依赖收集
trackRefValue(this)
return this._value
}
set value(newVal) {
// 值相同时不触发更新
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._value)) {
this._value = newVal
triggerRefValue(this) // 触发更新
}
}
}
// 使用示例
const count = ref(0)
// 等价于:const count = { value: 0 } + getter/setter拦截
ref vs reactive 对比:
| 对比维度 | ref | reactive |
|---|---|---|
| 适用类型 | 基本类型、对象 | 仅对象类型 |
| 访问方式 | 需要.value | 直接访问属性 |
| 重新赋值 | 可以替换整个.value | 不能替换整个对象 |
| 解构赋值 | 会丢失响应性 | 会丢失响应性 |
| 模板中使用 | 自动解包(顶层) | 无需解包 |
| TS支持 | 类型推断更友好 | 需要手动标注类型 |
4.3 computed:计算属性的魔法
源码文件: packages/reactivity/src/computed.ts
javascript
// Vue 3.4.31 computed类定义
class ComputedRefImpl {
private _value: T
private _dirty: boolean = true // 脏检查标志
public effect: ReactiveEffect
public dep?: Set<ReactiveEffect> = undefined
public readonly __v_isRef = true
constructor(getter, private readonly _setter, isReadonly) {
// 创建effect(懒执行 + scheduler)
this.effect = new ReactiveEffect(getter, () => {
// scheduler:依赖变化时标记为dirty
if (!this._dirty) {
this._dirty = true
triggerRefValue(this) // 通知依赖此computed的effects
}
})
this.effect.computed = this // 双向引用
}
get value() {
// 1. 依赖收集:让外层effect知道依赖了此computed
trackRefValue(this)
// 2. 脏检查:只有dirty时才重新计算
if (this._dirty) {
this._dirty = false
this._value = this.effect.run() // 执行getter函数
}
return this._value
}
set value(newValue) {
this._setter(newValue)
}
}
// 使用示例
const count = ref(0)
const double = computed(() => count.value * 2)
// 只有访问double.value时才会计算
// count变化时,double标记为dirty,下次访问时重新计算
计算属性的懒加载与缓存机制:
是
否
访问computed.value
是否dirty?
执行getter函数
返回缓存值
更新_value
标记为非dirty
返回计算结果
依赖变化
scheduler执行
标记为dirty
通知外层effects
五、依赖追踪的高级场景
5.1 嵌套响应式对象的依赖追踪
javascript
const state = reactive({
user: {
name: 'Alice',
age: 25
}
})
// 渲染函数依赖state.user.name
effect(() => {
console.log(state.user.name)
})
依赖收集过程:
- 第一次访问
state:track(target, 'user') - 第二次访问
state.user:track(target.user, 'name') - 最终依赖:
state → user → name → effect
5.2 数组响应式处理
javascript
const arr = reactive([1, 2, 3])
// 读取length会建立依赖
effect(() => console.log(arr.length)) // track(arr, 'length')
// 修改数组元素触发更新
arr[0] = 99 // trigger(arr, 'set', 0)
// 修改length触发更新
arr.length = 5 // trigger(arr, 'set', 'length')
// 添加元素触发更新
arr.push(4) // trigger(arr, 'add', 'length') + trigger(arr, 'add', 3)
数组特殊处理源码:
javascript
// packages/reactivity/src/baseHandlers.ts
const arrayInstrumentations = {
push(item) {
// 暂停依赖收集,避免内部操作误触发
toRaw(this).push(item)
},
pop() {
return toRaw(this).pop()
},
// ... 其他数组方法
}
5.3 循环依赖检测
Vue 3 使用 activeEffect 栈结构来检测循环依赖:
javascript
const effectStack: ReactiveEffect[] = []
function effect(fn) {
const _effect = new ReactiveEffect(fn)
_effect.run = function() {
// 1. 入栈
if (!effectStack.includes(_effect)) {
try {
effectStack.push(_effect)
activeEffect = _effect
return fn() // 执行用户函数
} finally {
// 2. 出栈
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
}
_effect.run()
}
// 循环依赖示例:
const count = ref(0)
effect(() => {
count.value++ // 会导致无限循环!
console.log(count.value)
})
// Vue 3会检测到循环依赖并警告
5.4 副作用清理机制
javascript
// Vue 3.4.31 effect清理逻辑
class ReactiveEffect {
deps: Array<Set<ReactiveEffect>> = []
run() {
// 1. 清理旧依赖
cleanupEffect(this)
// 2. 执行新函数,建立新依赖
return this.fn()
}
}
function cleanupEffect(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect) // 从依赖集合中移除自己
}
deps.length = 0 // 清空deps数组
}
}
清理时机:
- effect重新执行时
- effect.stop()手动停止时
- 组件卸载时(组件的渲染effect会自动清理)
六、性能优化与最佳实践
6.1 避免不必要的依赖收集
javascript
// ❌ 不好的做法:在条件分支中访问响应式数据
const state = reactive({ count: 0 })
effect(() => {
if (someCondition) {
console.log(state.count) // 只有条件为真时才建立依赖
}
})
// ✅ 好的做法:提前解构需要的属性
effect(() => {
const { count } = state
if (someCondition) {
console.log(count)
}
})
6.2 使用shallowRef优化大数据
javascript
// ❌ 深层响应式:处理大型数组性能差
const largeData = reactive({
items: new Array(10000).fill(0)
})
// ✅ 浅层响应式:只追踪.value的变化
const largeData = shallowRef({
items: new Array(10000).fill(0)
})
// 只有替换整个value时才触发更新
largeData.value = { items: [...] }
6.3 toRaw与markRaw
javascript
import { toRaw, markRaw } from 'vue'
// toRaw:获取原始对象(用于性能优化)
const state = reactive({ count: 0 })
const raw = toRaw(state)
console.log(raw === state) // false
// markRaw:标记对象永不响应式
const config = markRaw({ apiEndpoint: 'https://api.example.com' })
const app = reactive({
config // config不会被代理
})
toRaw vs markRaw 使用场景:
| 场景 | 推荐API | 原因 |
|---|---|---|
| 组件间传递响应式对象 | reactive/ref | 保持响应性 |
| 大型数据结构传递 | markRaw | 避免深度代理开销 |
| 第三方库对象 | markRaw | 避免与库内部逻辑冲突 |
| 需要直接修改原始数据 | toRaw | 绕过响应式系统 |
| 性能敏感的读写操作 | toRaw | 减少代理拦截开销 |
6.4 watchEffect vs watch
javascript
// watchEffect:自动收集依赖
watchEffect(() => {
console.log(state.count) // 自动追踪state.count
})
// watch:显式指定依赖
watch(() => state.count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
}, { immediate: true, deep: true })
watchEffect vs watch 对比:
| 对比维度 | watchEffect | watch |
|---|---|---|
| 依赖收集 | 自动追踪 | 显式指定 |
| 回调参数 | 无新值/旧值 | 有新值/旧值 |
| 初始执行 | 立即执行 | 可配置immediate |
| 清理副作用 | onInvalidate | 配置flush |
| 适用场景 | 简单副作用 | 需要新旧值对比 |
七、实战案例:构建一个响应式状态管理器
7.1 完整实现
javascript
// mini-store.js
import { reactive, computed, effect, toRaw } from 'vue'
export class createStore<T extends object>(state: T) {
// 1. 创建响应式状态
private state = reactive(state)
// 2. 存储所有effect(用于销毁)
private effects: Array<() => void> = []
// 3. 创建getter函数
getState(): Readonly<T> {
return this.state
}
// 4. 创建action工厂
defineAction<P extends any[]>(
name: string,
fn: (state: T, ...payload: P) => void
) {
return (...payload: P) => {
// 执行mutation
fn(this.state, ...payload)
}
}
// 5. 创建getter工厂(计算属性)
defineGetter<K>(name: string, fn: (state: T) => K) {
return computed(() => fn(this.state))
}
// 6. 订阅状态变化
subscribe(listener: (state: T, prevState: T) => void) {
let prevState = toRaw(this.state)
const dispose = effect(() => {
const currentState = toRaw(this.state)
listener(currentState, prevState)
prevState = currentState
})
this.effects.push(dispose)
return () => {
dispose()
this.effects = this.effects.filter(e => e !== dispose)
}
}
// 7. 销毁store
destroy() {
this.effects.forEach(dispose => dispose())
this.effects = []
}
}
// 使用示例
const store = new createStore({
count: 0,
user: { name: 'Guest' }
})
// 定义action
const increment = store.defineAction('increment', (state) => {
state.count++
})
const updateUser = store.defineAction('updateUser', (state, name) => {
state.user.name = name
})
// 定义getter
const doubleCount = store.defineGetter('doubleCount', (state) => state.count * 2)
// 订阅变化
store.subscribe((state, prevState) => {
console.log('State changed:', state)
})
// 使用
increment() // 触发订阅
console.log(doubleCount.value) // 2
7.2 依赖追踪流程图
Effect targetMap Reactive State Store 组件 Effect targetMap Reactive State Store 组件 getState() 返回响应式对象 访问state.count track(state, 'count') 记录当前渲染effect increment() 修改state.count++ trigger(state, 'count') 重新执行所有相关effects 触发组件重新渲染
八、Composition API与响应式系统的最佳实践
8.1 使用setup函数管理副作用
Vue 3的Composition API推荐使用<script setup>语法糖,它提供了更清晰的副作用管理方式:
vue
<script setup>
import { ref, watch, onMounted, onUnmounted } from 'vue'
// ✅ 好的做法:在setup顶层声明响应式变量
const count = ref(0)
const doubled = computed(() => count.value * 2)
// ✅ 明确的生命周期钩子
onMounted(() => {
console.log('Component mounted')
})
// ✅ 明确的清理逻辑
const stopWatch = watch(count, (newVal) => {
console.log('Count changed:', newVal)
})
onUnmounted(() => {
stopWatch() // 组件卸载时停止监听
})
</script>
响应式作用域规则:
- 在
<script setup>顶层创建的响应式变量会自动暴露给模板 - 在函数内部创建的响应式变量不会暴露给模板
- 使用
defineExpose()可以显式暴露某些变量给父组件
8.2 toRefs:解构时保持响应性
javascript
import { reactive, toRefs } from 'vue'
// ❌ 错误:直接解构会丢失响应性
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // count和name不再是响应式的
// ✅ 正确:使用toRefs保持响应性
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state)
// count和name现在是ref,可以单独使用
count.value++ // ✅ 会触发更新
// ✅ 在setup函数中返回toRefs
export function useCounter() {
const state = reactive({
count: 0,
doubled: computed(() => state.count * 2)
})
return {
...toRefs(state), // 解构时保持响应性
increment: () => state.count++
}
}
toRefs 实现原理:
javascript
// Vue 3.4.31 toRefs实现简化版
function toRefs(object) {
const ret = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
function toRef(object, key) {
return {
get value() {
return object[key]
},
set value(newVal) {
object[key] = newVal
}
}
}
8.3 响应式工具函数对比
| 工具函数 | 作用 | 使用场景 | 返回类型 |
|---|---|---|---|
reactive() |
创建深层响应式对象 | 复杂状态管理 | Proxy对象 |
ref() |
创建响应式引用 | 基本类型、需要替换整个对象 | Ref对象 |
computed() |
创建计算属性 | 派生状态、缓存计算结果 | ComputedRef |
readonly() |
创建只读代理 | 防止意外修改、传递给子组件 | Readonly对象 |
shallowReactive() |
创建浅层响应式 | 性能优化、大型数据结构 | Proxy对象(仅第一层) |
shallowRef() |
创建浅层ref | 性能优化、外部库集成 | Ref对象(.value不深层代理) |
toRaw() |
获取原始对象 | 性能优化、第三方库集成 | 原始对象 |
markRaw() |
标记永久非响应式 | 第三方库对象、复杂配置 | 原始对象 |
toRefs() |
转换为ref对象数组 | 解构响应式对象 | Ref对象集合 |
isProxy() |
判断是否为proxy | 调试、类型守卫 | boolean |
isReactive() |
判断是否为reactive | 调试、类型守卫 | boolean |
isReadonly() |
判断是否为readonly | 调试、类型守卫 | boolean |
8.4 响应式转换规则
Vue 3提供了unref()和isRef()工具来处理响应式和普通值的转换:
javascript
import { ref, unref, isRef } from 'vue'
const count = ref(0)
const num = 10
// unref:如果是ref则返回.value,否则返回原值
console.log(unref(count)) // 0
console.log(unref(num)) // 10
// isRef:类型守卫
function getValue(val) {
return isRef(val) ? val.value : val
}
// 实际应用:处理可能为ref或普通值的参数
function useLogCount(countRef) {
watch(() => unref(countRef), (val) => {
console.log('Count:', val)
})
}
useLogCount(ref(0)) // ✅ 可以传入ref
useLogCount(0) // ✅ 也可以传入普通值
8.5 响应式系统在组件通信中的应用
父子组件通信:props vs emit
vue
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const count = ref(0)
function handleUpdate(newVal) {
count.value = newVal
}
</script>
<template>
<Child :modelValue="count" @update:modelValue="handleUpdate" />
</template>
<!-- Child.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const localValue = computed({
get() {
return props.modelValue
},
set(newVal) {
emit('update:modelValue', newVal)
}
})
</script>
<template>
<input v-model="localValue" />
</template>
provide/inject:跨层级响应式数据共享
javascript
// 父组件提供响应式数据
import { provide, reactive, readonly } from 'vue'
const state = reactive({
user: { name: 'Alice', role: 'admin' }
})
// ✅ 提供只读版本,防止子组件直接修改
provide('user', readonly(state.user))
// ✅ 提供修改方法
provide('updateUser', (name) => {
state.user.name = name
})
// 子组件注入
import { inject } from 'vue'
const user = inject('user') // 只读
const updateUser = inject('updateUser') // 修改方法
// ✅ 使用inject默认值
const theme = inject('theme', 'light')
provide/inject vs Vuex/Pinia对比:
| 对比维度 | provide/inject | Vuex/Pinia |
|---|---|---|
| 适用范围 | 父子组件树 | 全局状态管理 |
| 数据流向 | 单向(父→子) | 多向(任意组件) |
| 响应式 | 需要手动提供reactive对象 | 自动响应式 |
| 调试工具 | 无 | 有(Vue DevTools) |
| TypeScript | 需要手动类型定义 | 完整类型支持 |
| 学习曲线 | 简单 | 相对复杂 |
九、响应式系统的性能优化深度解析
9.1 虚拟DOM与响应式系统的协作
是
否
是
否
数据变化
trigger执行
标记组件为dirty
scheduler调度
flush时机判断
pre flush?
DOM更新前执行
post flush?
DOM更新后执行
默认时机
组件重新渲染
虚拟DOM diff
真实DOM更新
flush策略详解:
javascript
import { watch, watchEffect } from 'vue'
// sync:同步执行(极少使用,可能导致性能问题)
watch(source, callback, { flush: 'sync' })
// pre:DOM更新前执行(默认,用于组件更新)
watch(source, callback, { flush: 'pre' })
// post:DOM更新后执行(用于DOM操作)
watch(source, callback, { flush: 'post' })
// 实际应用场景
watch(count, (newVal) => {
console.log('DOM更新前:', newVal)
}, { flush: 'pre' })
watch(count, (newVal) => {
console.log('DOM已更新,可以操作DOM')
document.title = `Count: ${newVal}`
}, { flush: 'post' })
9.2 批量更新与异步调度
Vue 3使用Promise.resolve()实现微任务级别的批量更新:
javascript
// Vue 3.4.31 scheduler核心逻辑(简化版)
const queue = []
let isFlushing = false
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
}
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs)
}
}
function flushJobs() {
for (const job of queue) {
job()
}
queue.length = 0
isFlushing = false
}
// 使用示例
const count = ref(0)
const name = ref('Vue')
// 这两个修改会被批处理为一次更新
count.value++
name.value = 'Vue 3'
// 只会触发一次组件重新渲染
批量更新的优势:
- 减少渲染次数
- 提高应用性能
- 避免中间状态闪烁
9.3 大列表优化:虚拟滚动与响应式
vue
<template>
<!-- ✅ 使用虚拟滚动 -->
<RecycleScroller
:items="largeList"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
</template>
<script setup>
import { ref } from 'vue'
// ❌ 不好的做法:直接响应式大型数组
const largeList = reactive(new Array(10000).fill(0).map((_, i) => ({
id: i,
name: `Item ${i}`
})))
// ✅ 好的做法:冻结初始数据,按需响应式
const initialData = Object.freeze(
new Array(10000).fill(0).map((_, i) => ({
id: i,
name: `Item ${i}`
}))
)
const largeList = ref(initialData)
// 只在整体替换时才触发更新
function updateList() {
largeList.value = [...newData]
}
</script>
9.4 响应式性能监控
javascript
import { onMounted, onUnmounted } from 'vue'
export function useReactivityPerformance() {
let trackCount = 0
let triggerCount = 0
onMounted(() => {
// 拦截track函数统计依赖收集次数
const originalTrack = window.__VUE_REACTIVITY_TRACK__
if (originalTrack) {
window.__VUE_REACTIVITY_TRACK__ = (...args) => {
trackCount++
return originalTrack(...args)
}
}
// 拦截trigger函数统计触发次数
const originalTrigger = window.__VUE_REACTIVITY_TRIGGER__
if (originalTrigger) {
window.__VUE_REACTIVITY_TRIGGER__ = (...args) => {
triggerCount++
return originalTrigger(...args)
}
}
})
onUnmounted(() => {
console.log('Performance Stats:')
console.log(`- Track calls: ${trackCount}`)
console.log(`- Trigger calls: ${triggerCount}`)
console.log(`- Ratio: ${(triggerCount / trackCount * 100).toFixed(2)}%`)
})
}
十、总结:掌握响应式系统的核心要点
10.1 关键技术点回顾
| 技术点 | 核心原理 | 注意事项 |
|---|---|---|
| Proxy拦截 | 拦截13种操作,实现精准依赖收集 | 兼容性:IE11不支持 |
| track/trigger | WeakMap + Map + Set三层结构 | 注意内存泄漏(及时cleanup) |
| effect栈 | 解决嵌套effect和循环依赖 | effectStack避免重入 |
| 脏检查 | computed通过_dirty标志实现缓存 | getter必须是纯函数 |
| 惰性代理 | 按需代理,减少初始化开销 | 首次访问才建立依赖 |
| 批量更新 | 微任务队列合并多次修改 | 减少渲染次数 |
| 调度器 | pre/post/sync三种flush策略 | 根据场景选择 |
10.2 响应式系统设计原则
1. 单向数据流原则
- 父组件向子组件传递props(只读)
- 子组件通过emit向父组件发送事件
- 避免直接修改props,遵循单向数据流
2. 最小化依赖原则
javascript
// ❌ 依赖整个对象
effect(() => {
console.log(state) // 任何属性变化都会触发
})
// ✅ 只依赖需要的属性
effect(() => {
console.log(state.count) // 只有count变化才触发
})
3. 纯函数原则
- computed的getter必须是纯函数
- 避免在getter中修改响应式数据
- 避免在getter中产生副作用(如API请求)
4. 及时清理原则
javascript
// ✅ 组件卸载时停止监听
onUnmounted(() => {
stopWatch()
clearInterval(timer)
})
10.3 性能优化清单
✅ 优先使用 reactive (对象类型)
✅ 基本类型使用 ref (避免包装对象)
✅ 大数据使用 shallowRef/shallowReactive (减少深度代理开销)
✅ 第三方库对象使用 markRaw (避免冲突)
✅ 避免在effect中直接修改响应式数据 (可能导致无限循环)
✅ 合理使用 computed (自动缓存,减少重复计算)
✅ 及时清理副作用 (组件卸载时停止watch/effect)
✅ 使用虚拟滚动处理大列表 (减少渲染节点)
✅ 利用批量更新机制 (合并多次修改)
✅ 选择合适的flush策略(pre/post/sync)
10.4 常见陷阱与解决方案
| 陷阱 | 问题表现 | 解决方案 |
|---|---|---|
| 解构丢失响应性 | 解构reactive对象后属性不再是响应式的 | 使用toRefs()或toRef() |
| 直接修改props | 警告且可能不生效 | 通过emit向父组件发送事件 |
| computed有副作用 | 违反纯函数原则 | 改用watch或在函数中处理 |
| 无限循环 | effect中修改依赖数据导致无限触发 | 使用stop()或调整逻辑 |
| 内存泄漏 | 未停止的watch/effect持续运行 | 在onUnmounted中清理 |
| 数组索引直接赋值 | arr[0] = value不触发响应式 |
使用arr.splice(0, 1, value) |
| 对象属性删除 | delete obj.key不触发响应式 |
使用Vue.delete()或Reflect.deleteProperty |
| watch旧值undefined | 首次执行时oldVal为undefined | 设置immediate: false或判断oldVal |
| 深层对象watch不触发 | 只监听顶层属性变化 | 使用deep: true或监听具体路径 |
| computed依赖不更新 | getter中访问非响应式数据 | 确保所有依赖都是响应式的 |
10.5 调试技巧与工具
1. Vue DevTools响应式面板
- 查看组件的响应式状态
- 追踪依赖关系
- 性能分析(渲染时间、更新次数)
2. console调试
javascript
import { reactive, isReactive, toRaw } from 'vue'
const state = reactive({ count: 0 })
// 检查是否为响应式对象
console.log('isReactive:', isReactive(state)) // true
// 获取原始对象
console.log('raw:', toRaw(state))
// 查看__v_internal属性(Vue内部)
console.log('internal:', state.__v_internal)
3. 性能分析
javascript
import { performance } from 'vue'
// 在Chrome DevTools中标记
performance.mark('update-start')
state.count++
performance.mark('update-end')
performance.measure('update', 'update-start', 'update-end')
const measures = performance.getEntriesByName('update')
console.log('Update took:', measures[0].duration, 'ms')
10.6 进阶学习路径
-
源码阅读顺序:
packages/reactivity/src/effect.ts(副作用系统)packages/reactivity/src/reactive.ts(响应式对象创建)packages/reactivity/src/baseHandlers.ts(Proxy处理器)packages/reactivity/src/computed.ts(计算属性)packages/runtime-core/src/scheduler.ts(调度器)packages/runtime-core/src/component.ts(组件更新)
-
调试技巧:
javascript
import { track, trigger } from 'vue'
// 在开发环境追踪依赖收集
const originalTrack = track
track = function(target, type, key) {
console.log('Track:', { target, type, key })
return originalTrack(target, type, key)
}
// 在开发环境追踪触发更新
const originalTrigger = trigger
trigger = function(target, type, key) {
console.log('Trigger:', { target, type, key })
return originalTrigger(target, type, key)
}
- 推荐资源:
- Vue 3 官方文档:深入响应式系统章节
- Vue 3 源码仓库:vuejs/core
- 技术博客:HcySunYang的Vue原理系列文章
- 视频:Vue Mastery - Advanced Vue 3 Features
- 书籍:《Vue.js设计与实现》(霍春阳)
10.7 Vue 3.4+ 新特性
1. 更好的TypeScript支持
typescript
import { ref } from 'vue'
// ✅ Vue 3.4+ 自动推断ref类型
const count = ref(0) // 自动推断为 Ref<number>
// ✅ 支持解构ref类型
type CountRef = typeof count // Ref<number>
2. 性能提升
- 响应式系统性能提升约40%
- 解析器速度提升约2倍
- 内存占用减少约20%
3. 新的API
javascript
import { onScopeDispose, getCurrentScope } from 'vue'
// ✅ 作用域自动清理
const scope = getCurrentScope()
onScopeDispose(() => {
console.log('Scope disposed')
})
4. 实验性功能
- Reactivity Transform(已废弃,推荐使用ref)
- Suspense组件(异步组件加载)
十一、总结与展望
11.1 核心要点回顾
通过本文的深入学习,我们了解了Vue 3响应式系统的核心机制:
- 基于Proxy的拦截机制:比Vue 2的Object.defineProperty更强大
- 精确的依赖追踪:track/trigger实现细粒度的依赖收集
- 高效的调度系统:批量更新、异步调度减少渲染次数
- 丰富的API生态:reactive、ref、computed等覆盖各种场景
- 完善的性能优化:惰性代理、缓存机制、调试工具
11.2 响应式系统的未来
1. Signals概念的兴起
- SolidJS、Preact、Angular都在采用类似方案
- Vue的响应式系统本质上是Signals的一种实现
- 未来可能更标准化、更高效
2. 编译时优化
- Vue 3.5+正在探索更多编译时优化
- 减少运行时开销
- 更好的Tree-shaking
3. 跨框架响应式
@vue/reactivity可以作为独立库使用- 在React、Angular中集成Vue的响应式系统
- 更强的可组合性
最后的建议:
理解响应式系统不仅仅是掌握API的使用,更是理解现代前端框架的核心思想------声明式编程。通过Proxy + Effect的组合,Vue 3实现了一套高效的依赖追踪机制,让我们能够专注于业务逻辑,而不用手动管理DOM更新。
但也要注意,响应式系统不是银弹。在性能敏感场景、大型数据处理、第三方库集成时,合理使用 toRaw、markRaw、shallowRef 等工具,才能发挥Vue 3的最大潜力。
希望本文能帮助你深入理解Vue 3的响应式系统,在实战中写出更优雅、更高效的代码! 🚀
参考源码版本: Vue 3.4.31