Vue3响应式原理深度解析:Proxy拦截与依赖收集机制
Vue3使用ES6 Proxy替代Vue2的Object.defineProperty实现响应式系统,解决了无法检测属性新增/删除和数组索引修改的局限性。理解Proxy拦截机制和依赖收集流程,对排查响应式失效问题和编写高性能组件至关重要。
Proxy代理对象的工作机制
Vue3的reactive()函数创建Proxy代理对象,拦截对目标对象的读取和修改操作:
// Vue3 reactive 简化实现
const targetMap = new WeakMap();
let activeEffect = null;
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(activeEffect);
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => effect());
}
}
function reactive(target) {
const handler = {
get(obj, key, receiver) {
const result = Reflect.get(obj, key, receiver);
track(target, key);
// 深层代理:对象类型属性递归创建Proxy
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(obj, key, value, receiver) {
const oldValue = obj[key];
const result = Reflect.set(obj, key, value, receiver);
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
};
return new Proxy(target, handler);
}
track在属性被读取时记录当前活跃的副作用函数(activeEffect),建立属性与依赖的映射关系。trigger在属性被修改时找到对应的依赖集合并执行所有副作用函数。依赖存储结构为三层:WeakMap(target) → Map(key) → Set(effect)。
ref与reactive的区别与转换
ref用于包装基本类型值,内部通过RefImpl类实现get/set拦截:
class RefImpl {
private _value;
private _rawValue;
public dep;
constructor(value) {
this._rawValue = value;
this._value = convert(value);
this.dep = new Set();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
if (newVal !== this._rawValue) {
this._rawValue = newVal;
this._value = convert(newVal);
triggerRefValue(this);
}
}
}
function convert(val) {
return typeof val === 'object' && val !== null
? reactive(val)
: val;
}
ref赋值对象类型时,内部调用reactive()转换。模板中ref自动解包(unwrap),但JavaScript代码中必须通过.value访问。watch和computed接收的值已经自动解包。
computed缓存的实现原理
computed具有缓存特性:依赖未变化时返回缓存值,避免重复计算。实现依赖dirty标志位:
class ComputedRefImpl {
private _dirty = true;
private _value;
private _effect;
constructor(getter) {
this._effect = new ReactiveEffect(getter, () => {
// 依赖变化时设置dirty,触发副作用
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;
}
set value(newVal) {
if (setter) setter(newVal);
}
}
首次访问.value时_dirty为true,执行getter计算并缓存结果。后续访问直接返回缓存。当依赖的响应式数据变化时,scheduler回调将_dirty设为true,下次访问时重新计算。多个组件引用同一个computed时,只计算一次。
watchEffect与watch的触发差异
watchEffect立即执行副作用函数,自动追踪函数内访问的响应式依赖:
// watchEffect:自动收集依赖,立即执行
const count = ref(0);
const name = ref('test');
watchEffect(() => {
console.log(count.value, name.value);
});
// 输出:0 test
count.value = 1;
// 输出:1 test
// watch:显式指定侦听源,获取新旧值
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`);
}, { immediate: false, deep: false });
// 多源侦听
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount}→${newCount}, name: ${oldName}→${newName}`);
});
watch的deep: true选项对reactive对象执行深层遍历。深层watch会递归访问对象所有属性触发track,性能开销与对象深度成正比。对大型嵌套对象,优先使用watchEffect只追踪实际访问的属性。
响应式失效场景排查
常见响应式失效原因及解决方案:
场景1:解构reactive对象丢失响应性
const state = reactive({ count: 0, name: 'test' });
// 错误:解构后count是基本类型,失去响应性
const { count, name } = state;
// 正确方案1:使用toRefs保持响应性
const { count, name } = toRefs(state);
// count.value, name.value 仍是响应式
// 正确方案2:直接通过state访问
state.count
场景2:直接替换reactive对象
let state = reactive({ count: 0 });
// 错误:重新赋值后state指向新对象,旧Proxy被丢弃
state = reactive({ count: 1 });
// 正确方案1:使用Object.assign
Object.assign(state, { count: 1 });
// 正确方案2:改用ref
const state = ref({ count: 0 });
state.value = { count: 1 }; // ref的set会触发更新
场景3:数组操作未触发更新
const list = reactive([1, 2, 3]);
// Vue3中Proxy能正确拦截数组方法,以下操作均有效:
list.push(4); // 触发更新
list.splice(0, 1); // 触发更新
list[0] = 99; // 触发更新(Vue2中无效)
list.length = 0; // 触发更新(Vue2中无效)
性能优化:shallowRef与shallowReactive
深层响应式代理会对对象递归创建Proxy,大型数据结构(如10000条记录的列表)初始化时产生明显开销。shallowReactive只代理第一层属性:
import { shallowReactive, shallowRef, triggerRef } from 'vue';
// shallowReactive:只代理根级属性
const state = shallowReactive({
list: [], // 修改list不触发更新(除非替换整个list引用)
count: 0 // 修改count触发更新
});
state.count = 1; // 触发更新
state.list.push(1); // 不触发更新
state.list = [1, 2, 3]; // 触发更新(替换引用)
// shallowRef + triggerRef:手动控制触发时机
const data = shallowRef({ items: [] });
// 批量操作后手动触发
data.value.items.push(...batchData);
triggerRef(data); // 一次性触发更新,避免多次渲染
表格组件渲染10000行数据时,使用shallowRef存储数据 + triggerRef手动触发更新,初始化时间从800ms降至50ms。配合v-memo指令缓存行渲染结果,进一步减少虚拟DOM diff开销:
<tr v-for="item in data.items" :key="item.id" v-memo="[item.id, item.updated]">
<td>{{ item.name }}</td>
<td>{{ item.value }}</td>
</tr>
v-memo数组中的依赖未变化时,该行跳过diff直接复用上次渲染结果。数据更新时只有item.updated发生变化的行重新渲染,其余行复用VNode。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-xiang-ying-shi-yuan-li-shen-du-jie-xi-proxy-lan-jie-yu/