Vue 3 响应式原理
Vue 3 的响应式系统基于 ES6 Proxy,相比 Vue 2 的 Object.defineProperty,能拦截更多操作,且无需预先声明属性。
核心:Proxy + 依赖追踪
js
// Vue 3 响应式的最小实现
let activeEffect = null // 当前正在执行的副作用
// 依赖收集容器:WeakMap<target, Map<key, Set<effect>>>
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 deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
deps?.forEach(effect => effect())
}
function reactive(raw) {
return new Proxy(raw, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 收集依赖
return result
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
}
})
}
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn() // 执行时会触发 getter,自动收集依赖
activeEffect = null
}
effectFn()
return effectFn
}
// 使用
const state = reactive({ count: 0, name: 'Vue' })
effect(() => {
console.log(`count is ${state.count}`) // 自动追踪 count
})
state.count++ // 触发上面的 effect → 'count is 1'ref vs reactive
ts
import { ref, reactive, computed, watch, watchEffect } from 'vue'
// ref:包装任意值(基本类型必须用 ref)
const count = ref(0)
count.value++ // 通过 .value 访问
// 模板中自动解包,不需要 .value
// reactive:包装对象(深度响应式)
const state = reactive({
user: { name: 'Alice', age: 30 },
posts: []
})
state.user.name = 'Bob' // 深度响应,直接修改
// ⚠️ reactive 的限制
const { user } = state // ❌ 解构后失去响应性
const { user } = toRefs(state) // ✅ toRefs 保持响应性
// ref 的底层:对象类型也用 Proxy
const obj = ref({ count: 0 })
// obj.value 是一个 reactive 对象computed
ts
const firstName = ref('John')
const lastName = ref('Doe')
// 只读 computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写 computed
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const [first, last] = val.split(' ')
firstName.value = first
lastName.value = last
}
})
fullNameWritable.value = 'Jane Smith'
// firstName.value → 'Jane', lastName.value → 'Smith'
// computed 是惰性的:只有被访问时才计算,且会缓存结果
// 只有依赖变化时才重新计算watch & watchEffect
ts
const count = ref(0)
const user = reactive({ name: 'Alice', age: 30 })
// watch:明确指定监听源,懒执行(默认不立即执行)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
// 监听多个源
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('变化了')
})
// 深度监听对象
watch(user, (newUser) => {
console.log('user 变化:', newUser)
}, { deep: true })
// 立即执行
watch(count, (val) => { /* ... */ }, { immediate: true })
// watchEffect:自动追踪依赖,立即执行
const stop = watchEffect(() => {
// 自动追踪 count.value 和 user.name
console.log(`${user.name}: ${count.value}`)
})
// 停止监听
stop()
// watchEffect 的清理
watchEffect((onCleanup) => {
const timer = setInterval(() => { /* ... */ }, 1000)
onCleanup(() => clearInterval(timer)) // 下次执行前或停止时清理
})Vue 2 vs Vue 3 响应式对比
js
// Vue 2:Object.defineProperty 的局限
const vm = new Vue({
data: { user: { name: 'Alice' } }
})
// ❌ 无法检测属性添加
vm.user.age = 30 // 不是响应式的!
Vue.set(vm.user, 'age', 30) // 必须用 Vue.set
// ❌ 无法检测数组索引赋值
vm.list[0] = 'new' // 不是响应式的!
vm.$set(vm.list, 0, 'new') // 必须用 $set
// Vue 3:Proxy 完全解决这些问题
const state = reactive({ user: { name: 'Alice' } })
state.user.age = 30 // ✅ 自动响应式
state.list = []
state.list[0] = 'new' // ✅ 自动响应式
delete state.user.name // ✅ 删除也能检测响应式工具函数
ts
import { isRef, isReactive, isProxy, toRaw, markRaw, shallowRef, shallowReactive } from 'vue'
// 检测
isRef(count) // true
isReactive(state) // true
isProxy(state) // true
// toRaw:获取原始对象(跳过响应式,用于性能敏感操作)
const raw = toRaw(state)
// markRaw:标记对象永远不转为响应式
const chart = markRaw(new ECharts()) // 第三方库实例不需要响应式
// shallowRef / shallowReactive:浅层响应式(性能优化)
const shallowState = shallowReactive({
nested: { count: 0 } // nested 内部不是响应式的
})实战:自定义响应式 Hook
ts
// useMouse:追踪鼠标位置
function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e: MouseEvent) {
x.value = e.clientX
y.value = e.clientY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
// useAsync:异步数据获取
function useAsync<T>(fn: () => Promise<T>) {
const data = ref<T | null>(null)
const loading = ref(true)
const error = ref<Error | null>(null)
fn()
.then(result => { data.value = result })
.catch(err => { error.value = err })
.finally(() => { loading.value = false })
return { data, loading, error }
}总结
- Vue 3 响应式基于 Proxy,能拦截属性增删和数组索引操作
ref用于基本类型,reactive用于对象(解构用toRefs)computed是惰性缓存的,watchEffect自动追踪依赖markRaw和shallowReactive用于性能优化- 响应式系统与组件解耦,可以在任何地方使用