Vue 3 用 Proxy 重写了响应式系统,性能与能力都大幅提升。本文从源码设计角度剖析依赖收集、触发更新、副作用调度和计算属性的实现原理,并手写一个迷你响应式系统。
一、Vue 2 vs Vue 3 响应式对比
| 特性 | Vue 2 (defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 属性新增/删除 | 需要 Vue.set/Vue.delete | 直接支持 |
| 数组索引赋值 | 不支持 | 支持 |
| Map/Set | 不支持 | 支持 |
| 初始化 | 递归遍历所有属性 | 惰性代理,访问时才转换 |
| 性能 | 大对象初始化慢 | 按需代理,更快 |
// Vue 2 的局限
this.obj.newProp = 'value' // ❌ 不触发更新
this.arr[0] = newItem // ❌ 不触发更新
this.arr.length = 0 // ❌ 不触发更新
// Vue 3 全部支持
state.newProp = 'value' // ✅
state.arr[0] = newItem // ✅
state.arr.length = 0 // ✅
state.map.set('key', val) // ✅二、Proxy 基础
const raw = { price: 100, quantity: 2 }
const proxy = new Proxy(raw, {
get(target, key, receiver) {
console.log(`读取 ${key}`)
track(target, key) // 依赖收集
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置 ${key} = ${value}`)
const result = Reflect.set(target, key, value, receiver)
trigger(target, key) // 触发更新
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
trigger(target, key)
return result
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
}
})
proxy.price // 读取 price
proxy.quantity = 3 // 设置 quantity = 3为什么用 Reflect? 保证 this 指向代理对象,让内嵌对象也能被正确追踪:
const obj = reactive({
nested: { count: 0 } // nested 会被惰性转换为响应式
})
// obj.nested 访问时返回嵌套对象的代理
// 这是 Vue 3 实现深层响应式的关键三、手写迷你响应式系统
1. 副作用函数与依赖收集
let activeEffect = null
const targetMap = new WeakMap()
// 追踪依赖
function track(target, key) {
if (!activeEffect) return
// targetMap: target → key → Set<effect>
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(activeEffect)
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const deps = depsMap.get(key)
if (!deps) return
// 复制一份,避免无限循环
const effectsToRun = new Set(deps)
effectsToRun.forEach(effect => {
// 避免递归调用自身
if (effect !== activeEffect) {
effect()
}
})
}2. effect 函数
function effect(fn) {
const effectFn = () => {
// 清除旧依赖(解决分支切换问题)
cleanup(effectFn)
activeEffect = effectFn
fn()
activeEffect = null
}
effectFn.deps = []
effectFn()
return effectFn
}
function cleanup(effectFn) {
effectFn.deps.forEach(depSet => {
depSet.delete(effectFn)
})
effectFn.deps.length = 0
}3. reactive 实现
const reactiveMap = new Map()
function reactive(target) {
// 同一对象只代理一次
if (reactiveMap.has(target)) {
return reactiveMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
if (key === '__isReactive') return true
track(target, key)
const result = Reflect.get(target, key, receiver)
// 惰性深层代理:嵌套对象访问时才转换
if (typeof result === 'object' && result !== null) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const type = Object.prototype.hasOwnProperty.call(target, key)
? 'SET' : 'ADD'
const result = Reflect.set(target, key, value, receiver)
// 值变化才触发
if (oldValue !== value || (typeof oldValue === 'NaN' && typeof value === 'NaN')) {
trigger(target, key, type)
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
trigger(target, key, 'DELETE')
}
return result
}
})
reactiveMap.set(target, proxy)
return proxy
}4. ref 实现
function ref(value) {
return {
get value() {
track(this, 'value')
return value
},
set value(newValue) {
if (newValue !== value) {
value = newValue
trigger(this, 'value')
}
}
}
}5. computed 实现
function computed(getter) {
let value
let dirty = true // 脏标记:是否需要重新计算
// 作为副作用运行,依赖变化时标记为脏
const effectFn = effect(() => {
// 每次依赖变化重新执行,但不会立即触发渲染
if (dirty) {
value = getter()
}
})
// 依赖变化时置脏
// (简化实现,真实实现用 scheduler)
return {
get value() {
if (dirty) {
value = getter()
dirty = false
}
return value
}
}
}更接近真实的 computed:
function computed(getter) {
let value
let dirty = true
const runner = effect(getter, {
lazy: true,
// scheduler:依赖变化时不立即执行,只标记脏
scheduler() {
if (!dirty) {
dirty = true
// 通知依赖该 computed 的渲染副作用更新
triggerRefValue(this)
}
}
})
return {
get value() {
if (dirty) {
value = runner()
dirty = false
}
// 让当前 activeEffect 追踪此 computed
trackRefValue(this)
return value
}
}
}四、副作用调度与批更新
Vue 3 的组件更新是异步批处理的:
const queue = new Set()
let isFlushing = false
function queueJob(job) {
queue.add(job) // Set 自动去重,同一组件多次修改只更新一次
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs)
}
}
function flushJobs() {
// 按组件 id 排序,父组件先更新
const jobs = [...queue].sort((a, b) => a.id - b.id)
queue.clear()
isFlushing = false
jobs.forEach(job => job())
}这就是为什么修改多次数据只触发一次渲染:
import { ref, nextTick } from 'vue'
const count = ref(0)
// 触发 3 次修改,但只渲染 1 次
count.value++
count.value++
count.value++
// DOM 更新是异步的
console.log(document.querySelector('#count').textContent) // 还是旧值
nextTick(() => {
console.log(document.querySelector('#count').textContent) // "3"
})五、Vue 3 编译优化
静态提升(hoistStatic)
// 模板中的静态节点会被提升到 render 函数外部
// 只创建一次,重复渲染时直接复用
// 编译前
// <div>
// <span class="static">静态内容</span>
// <span>{{ dynamic }}</span>
// </div>
// 编译后
const _hoisted_1 = createVNode("span", { class: "static" }, "静态内容")
function render() {
return createVNode("div", null, [
_hoisted_1, // 直接复用
createVNode("span", null, toDisplayString(dynamic))
])
}Patch Flags(补丁标记)
// 动态节点带上标记,diff 时只对比标记的部分
createVNode("span", null, text, 1 /* TEXT */)
createVNode("span", { class: dynamicClass }, null, 2 /* CLASS */)
createVNode("span", { id: dynamicId }, null, 4 /* PROPS */, ["id"])
// PatchFlag 枚举:
// TEXT = 1 只有文本变化
// CLASS = 2 只有 class 变化
// STYLE = 4 只有 style 变化
// PROPS = 8 只有 props 变化
// FULL_PROPS = 16 props 有动态 key
// NEED_HYDRATION = 32
// STABLE_FRAGMENT = 64
// KEYED_FRAGMENT = 128 带 key 的 fragment
// ...Block Tree(块树)
// 动态节点被收集到 block 中,diff 时跳过静态内容
function render() {
return (openBlock(), createElementBlock("div", null, [
// 大量静态节点...
createVNode("span", null, dynamic, 1 /* TEXT */)
// 这个动态节点会被"block"追踪
// 更新时直接定位,不需要遍历整棵树
]))
}六、调试响应式
onTrack 与 onTrigger
// 仅开发模式可用
const state = reactive({ count: 0 }, {
onTrack(event) {
// 依赖被追踪时
console.log('track:', event.key, event.target)
},
onTrigger(event) {
// 依赖变化触发时
console.log('trigger:', event.key, event.type, event.newValue)
}
})
watch(source, callback, {
onTrack(e) { debugger },
onTrigger(e) { debugger }
})常见响应式陷阱
// 1. 解构 reactive 丢失响应式
const state = reactive({ count: 0 })
let { count } = state // ❌ count 是普通值
const { count } = toRefs(state) // ✅ 保持响应式
// 2. ref 在模板中自动解包,在 JS 中需要 .value
const count = ref(0)
console.log(count) // Ref 对象
console.log(count.value) // 0
// 3. reactive 被整体替换
let state = reactive({ list: [] })
state = reactive({ list: [1] }) // ❌ 旧引用的依赖全部失效
// ✅ 用 ref 包裹,或只修改属性
// 4. markRaw 跳过转换
const state = reactive({
chart: markRaw(new ECharts()) // ✅ 第三方实例不代理
})七、小结
Vue 3 响应式系统的核心是 Proxy 拦截 + WeakMap 存储依赖 + 副作用调度。理解 track/trigger 机制、惰性深层代理和异步批更新,不仅能帮你避开响应式陷阱,也是进阶 Vue 源码的第一步。手写迷你版响应式系统是最好的学习方式。
本文由 inspirecl.asia 原创,转载请注明出处。
评论 (0)