Appearance
✨ 响应式数据的本质 👌
要点速览
- “响应式”本质是“对数据操作的拦截”,在拦截里执行我们的业务逻辑(依赖收集、调度更新等)。
ref用“访问器属性”的get/set拦截对value的读写;reactive用Proxy拦截对象成员的读写、枚举、删除、原型等操作。- 是否会拦截的核心判断:操作是否发生在“被拦截对象的成员上”。非成员操作(如变量重新赋值)不会触发拦截。
- 同一数据上的不同操作触发的是“不同的拦截逻辑”,不要期待一个统一拦截能搞定全部行为。
学习目标
- 看懂
ref与reactive的拦截点与触发条件。 - 会判断某个操作是否“发生在成员上”,从而是否会触发拦截逻辑。
- 能区分:在
ref包裹对象上改“对象成员”,这是reactive的拦截,不是ref的拦截。
什么是响应式数据?
- 能拦截我们对数据的操作(读、写、删、枚举等),并在拦截逻辑里执行我们写好的处理,即可称为“响应式数据”。
- Vue 以
ref与reactive两条路径产出“可拦截的对象”,从而具备响应能力。
为什么要学会判断“是否会拦截”?
- 拦截逻辑是分散在不同操作上的,不是单一入口;不同操作触发不同拦截代码。
- 并非所有操作都能拦截:例如变量重新赋值、对不可拦截类型的“成员操作”等。
- 学会判断能避免“以为会更新、结果没触发”的尴尬,用正确方式驱动更新。
ref 的拦截原理(访问器属性)
这是一个简化版的 ref 实现思路:通过“访问器成员 value 的 get 与 set”拦截对 value 的读写。
js
class RefImpl {
constructor(value) {
// 简化:对象则交给 reactive 处理;原始值直接保存
this._value =
typeof value === "object" && value !== null ? reactive(value) : value;
this.__v_isRef = true;
}
get value() {
console.log("getter value", this._value);
// 读取时可进行依赖收集(此处演示略)
return this._value;
}
set value(newVal) {
console.log("setter newVal", newVal);
// 写入时可做变更判断与派发更新(此处演示略)
this._value = newVal;
}
}
function ref(value) {
return new RefImpl(value);
}结论
ref的拦截发生在对“refObj.value的读写”这一成员操作上。- 写入时可做“变更判断”以避免不必要的更新;读取时可做“依赖收集”。
ref:是否触发拦截的演示
js
// 创建拦截对象
let state = ref({ name: "zhangsan" });
// 变量重新赋值:不是成员操作,不触发 ref 的拦截
state = { name: "lisi" };
// 成员写入:发生在 ref 对象的成员上,触发拦截
state.value = { name: "lisi" };
// 对 value(对象)的成员写入:这是 reactive 的拦截,不是 ref 的拦截
state.value.name = "wangwu";
// 写入字符串:只触发 ref 的 set;字符串的“成员拦截”不存在
state.value = "abc";reactive 的拦截原理(Proxy)
使用 Proxy 对对象进行拦截,常见是 get 与 set 两个成员操作的入口:
js
const state = { name: "Bob" };
const proxyCache = new WeakMap();
function reactive(target) {
if (typeof target !== "object" || target === null) {
return target;
}
if (proxyCache.has(target)) {
return proxyCache.get(target);
}
const handler = {
// 读取成员拦截
get(obj, key, receiver) {
const v = Reflect.get(obj, key, receiver);
console.log("get 拦截逻辑", key, v);
return typeof v === "object" && v !== null ? reactive(v) : v;
},
// 写入成员拦截
set(obj, key, val, receiver) {
const prev = Reflect.get(obj, key, receiver);
const ok = Reflect.set(obj, key, val, receiver);
if (ok && prev !== val) {
console.log("set 拦截逻辑成功");
}
return ok;
},
deleteProperty(obj, key) {
const ok = Reflect.deleteProperty(obj, key);
if (ok) {
console.log("delete 拦截逻辑成功");
}
return ok;
},
// ...
};
const proxy = new Proxy(target, handler);
proxyCache.set(target, proxy);
return proxy;
}
const proxyState = reactive(state);
console.log(proxyState.name); // 触发 get
proxyState.name = "Alice"; // 触发 set进一步拦截点
- 除
get/set外,Vue 在reactive中还会拦截:has、deleteProperty、ownKeys、getOwnPropertyDescriptor等,以保证与原对象的行为一致并维护不变式。 - 枚举相关建议使用
Reflect.ownKeys保持 string 与 Symbol 键的一致性。
如何判断“是否会拦截”?
核心标准:操作是否发生在“拦截对象的成员上”。下面用一组示例来直观判断:
jsx
// 示例 1:对象成员读写 → 触发 reactive 的 get/set
const proxy = new Proxy({ name: 'Ak' }, {
get(t, k) { console.log('get', k); return t[k] },
set(t, k, v) { console.log('set', k, v); t[k] = v; return true }
})
console.log(proxy.name) // get
proxy.name = 'Sky' // set
// 示例 2:ref 的成员写入 → 触发 ref 的 set
const count = ref(0)
count.value = 1
// 示例 3:ref 包裹对象成员写入 → 触发 reactive 的 set(不是 ref 的拦截)
const user = ref({ name: 'A' })
user.value.name = 'B'
// 示例 4:字符串成员不可拦截 → 仅触发 ref 的 set
const title = ref('hello')
title.value = 'world'
// 示例 5:数组操作(push)→ 内部会触发若干 set(索引与 length)
const list = new Proxy([], {
set(t, k, v) { console.log('array set', k, v); t[k] = v; return true }
})
list.push(4)学会判断拦截
- 变量重新赋值不是成员操作,不会触发拦截。
- 在
ref包裹的对象上改“对象成员”,触发的是reactive的拦截。 - 基本类型的“成员操作”不可拦截(字符串、数字、布尔)。
常见坑位
易错点
- 把“非成员操作”误认为会触发拦截,导致更新逻辑没有执行。
- 在
ref包裹对象上做成员写入,却以为是ref的拦截;实际是reactive的拦截路径。 - 忘记通过
state.value访问ref的值,直接state = ...导致不会触发拦截。 - 枚举对象键时用
Object.keys,忽略了Symbol键与不可枚举键;建议Reflect.ownKeys(配合 Proxy 的ownKeys)。
枚举对象键的区别
js
Object.keys() - 只枚举可枚举的字符串键,不包括 Symbol
Object.getOwnPropertyNames() - 枚举所有字符串键(包括不可枚举),不包括 Symbol
Object.getOwnPropertySymbols() - 专门枚举 Symbol 键
Reflect.ownKeys() - 枚举所有键(字符串 + Symbol)使用建议
- 优先用
reactive管对象/数组的成员操作,用ref管标量或需要“引用语义”的场景。 - 判断拦截从“是否发生在成员上”入手,再结合对象是否被代理、成员类型是否可拦截。
- 与渲染更新配合时,建议使用“微任务合并 + 调度”的模式,避免多次同步更新导致抖动。
小结
- 响应式的本质是“拦截数据操作并在拦截里执行处理”。
ref与reactive的拦截点不同:分别拦截value的读写与对象成员的读写/枚举等。- 是否会拦截,取决于操作是否发生在“拦截对象的成员上”。学会正确判断,才能稳定驱动更新与行为。
