代理和反射
JavaScript 中的 Proxy
和 Reflect
是两个强大的内置对象,它们为对象操作提供了更灵活的控制机制。Proxy
允许我们创建一个对象的代理,从而可以拦截和自定义对象的基本操作(如属性查找、赋值、枚举等)。而 Reflect
则提供了一套与 Proxy
处理器方法一一对应的 API,使得我们能够方便地重建被拦截方法的原始行为。这两个特性在现代 JavaScript 开发中扮演着重要角色,特别是在实现响应式系统、数据验证、日志记录等场景中。
代理 Proxy
代理是目标对象的抽象,它允许我们为对象创建一个代理,从而可以拦截和自定义对象的基本操作。在对目标对象的各种操作影响目标对象之前,我们可以在代理对象中对这些操作加以控制。
创建代理
代理是通过 Proxy
构造函数创建的,该构造函数接收两个参数:目标对象和处理程序对象。
// 1. 目标对象
const target = {
id: "target",
};
// 2. 处理程序对象
const handler = {};
// 3. 创建代理对象
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = "foo";
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = "bar";
console.log(target.id); // bar
console.log(proxy.id); // bar
// hasOwnProperty() 方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty("id")); // true
console.log(proxy.hasOwnProperty("id")); // true
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
捕获器(Trap)
捕获器是在处理程序对象中定义的"基本操作的拦截器"。每个捕获器都可以拦截特定的操作。
// 目标对象
const target = {
foo: "bar",
};
// 处理程序对象
const handler = {
// 定义一个 get() 捕获器,捕获器在处理程序对象中以方法名为键
get(trapTarget, property, receiver) {
return "handler override";
},
};
// 创建代理对象
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override
// get() 捕获器会接收到目标对象、要查询的属性和代理对象三个参数
// get(trapTarget, property, receiver) {};
使用 Reflect 重建原始行为
Reflect
API 提供了与捕获器拦截方法相同名称和签名的对应方法,使得重建原始行为变得简单。
const target = {
foo: "bar",
};
const handler = {
get(trapTarget, property, receiver) {
return Reflect.get(...arguments);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
有了三个参数,重建被捕获方法的原始行为
// 重建被捕获方法的原始行为
const target = {
foo: "bar",
};
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
反射 Reflect
Reflect
API 提供了一套与 Proxy
处理器方法一一对应的 API,使得我们能够方便地重建被拦截方法的原始行为。
const target = {
foo: "bar",
};
const handler = {
get() {
console.log(...arguments); // { foo: 'bar' } foo { foo: 'bar' }
return Reflect.get(...arguments);
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
// 简写形式
const proxy = new Proxy(target, Reflect);
使用反射重建被捕获方法的原始行为
const target = {
foo: "bar",
baz: "qux",
};
const handler = {
get(trapTarget, property, receiver) {
let decoration = "";
if (property === "foo") {
decoration = "!!!";
}
return Reflect.get(...arguments) + decoration;
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar!!!
console.log(target.foo); // bar
console.log(proxy.baz); // qux
console.log(target.baz); // qux
代理捕获器与反射方法
代理可以捕获 13 种不同的基本操作:
get(target, property, receiver)
set(target, property, value, receiver)
has(target, property)
defineProperty(target, property, descriptor)
getOwnPropertyDescriptor()
deleteProperty()
ownKeys()
getPrototypeOf()
setPrototypeOf()
isExtensible()
preventExtensions()
apply()
construct()
常见代理模式
1. 跟踪属性访问
const user = {
name: "Jake",
};
const proxy = new Proxy(user, {
get(target, property, receiver) {
console.log(`Getting ${property}`);
return Reflect.get(...arguments);
},
set(target, property, value, receiver) {
console.log(`Setting ${property}=${value}`);
return Reflect.set(...arguments);
},
});
proxy.name; // Getting name
proxy.age = 27; // Setting age=27
2. 隐藏属性
const hiddenProperties = ["foo", "bar"];
const targetObject = {
foo: 1,
bar: 2,
baz: 3,
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined;
}
return Reflect.get(...arguments);
},
has(target, property) {
if (hiddenProperties.includes(property)) {
return false;
}
return Reflect.has(...arguments);
},
});
console.log(proxy.foo); // undefined
console.log(proxy.baz); // 3
console.log("foo" in proxy); // false
console.log("baz" in proxy); // true
3. 数据绑定与可观察对象
const userList = [];
function emit(newValue) {
console.log(newValue);
}
const proxy = new Proxy(userList, {
set(target, property, value, receiver) {
const result = Reflect.set(...arguments);
if (result) {
emit(Reflect.get(target, property, receiver));
}
return result;
},
});
proxy.push("John"); // John, 1
proxy.push("Jacob"); // Jacob, 2
这种模式在响应式系统中特别有用,比如 Vue 3 的响应式系统就是基于类似的原理实现的。
Vue3 中的代理与反射
Vue3 的响应式系统是基于 Proxy
和 Reflect
实现的,这是一个非常典型的应用场景。让我们来看看它的核心实现原理。
1. 响应式原理
// 简化版的 Vue3 响应式实现
function reactive(target) {
const handler = {
get(target, key, receiver) {
// 依赖收集
track(target, key);
// 使用 Reflect 获取属性值
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
// 获取旧值
const oldValue = target[key];
// 使用 Reflect 设置属性值
const result = Reflect.set(target, key, value, receiver);
// 如果值发生变化,触发更新
if (oldValue !== value) {
trigger(target, key);
}
return result;
},
};
return new Proxy(target, handler);
}
// 使用示例
const state = reactive({
count: 0,
});
2. 依赖收集与触发更新
在 Vue3 的响应式系统中,track
和 trigger
是两个核心函数:
- track(依赖收集)
- 用于收集当前正在执行的副作用函数(如 computed、watch 等)
- 建立数据与副作用函数之间的依赖关系
- 当访问响应式数据时触发
// 简化的 track 实现
const targetMap = new WeakMap();
const activeEffect = null;
function track(target, key) {
if (!activeEffect) return;
// 获取 target 的依赖 Map
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// 获取 key 的依赖 Set
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 将当前副作用函数添加到依赖中
deps.add(activeEffect);
}
- trigger(触发更新)
- 用于触发依赖该数据的副作用函数重新执行
- 当响应式数据发生变化时触发
- 会遍历并执行所有相关的副作用函数
// 简化的 trigger 实现
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
// 执行所有依赖该数据的副作用函数
deps.forEach((effect) => effect());
}
}
- 副作用函数的注册
- 使用
effect
函数包装副作用函数 - 在副作用函数执行前,将其设置为当前活动的副作用
- 使用
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
fn();
activeEffect = null;
};
effectFn();
}
// 使用示例
effect(() => {
console.log(state.count); // 会触发 track
});
state.count = 1; // 会触发 trigger
这种依赖收集和触发更新的机制使得 Vue3 能够:
- 精确追踪数据的变化
- 按需更新相关的视图
- 避免不必要的更新
- 支持更复杂的响应式场景
3. 实际应用示例
// 创建一个响应式对象
const state = reactive({
user: {
name: "John",
age: 30,
},
todos: [],
});
// 添加一个计算属性
const userInfo = computed(() => {
return `${state.user.name} is ${state.user.age} years old`;
});
// 监听变化
watch(
() => state.user.name,
(newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`);
}
);
// 修改属性会触发响应式更新
state.user.name = "Jane"; // 触发 watch
console.log(userInfo.value); // "Jane is 30 years old"
// 数组操作也会被正确追踪
state.todos.push({ id: 1, text: "Learn Vue3" });
4. 注意事项
响应式转换
- 只有通过
reactive()
或ref()
包装的对象才会变成响应式 - 直接修改原始对象不会触发响应式更新
- 只有通过
解构问题
- 解构响应式对象会失去响应性
- 需要使用
toRefs
来保持响应性
// ❌ 错误示例:解构会失去响应性
const { name, age } = state.user;
// ✅ 正确示例:使用 toRefs 保持响应性
const { name, age } = toRefs(state.user);
- 性能优化
- 避免创建过深的响应式对象
- 合理使用
shallowReactive
和shallowRef
- 对于不需要响应式的数据,不要使用响应式 API