"红宝书" 通常指的是《JavaScript 高级程序设计》,这是一本由 Nicholas C. Zakas(尼古拉斯·扎卡斯)编写的 JavaScript 书籍,是一本广受欢迎的经典之作。这本书是一部翔实的工具书,满满的都是 JavaScript 知识和实用技术。
不管你有没有刷过红宝书,如果现在还没掌握好,那就一起来刷红宝书吧,go!go!go!
系列文章:
第一部分:基本知识(重点、反复阅读)
第二部分:进阶内容(重点、反复阅读)
第 9 章 代理与反射
代理(Proxy)和反射(Reflect)是 ECMAScript 6(ES6)引入的两个新特性,用于操作和拦截 JavaScript 对象的行为。
概念
代理(Proxy):
代理是一个用于定义基本操作行为的对象,它允许你在对象上创建一个代理层,以拦截和定制对象的操作。代理对象可以用来拦截对目标对象的访问、修改、添加、删除等操作
。
反射(Reflect):
反射是一组新的内置对象和方法,它提供了对对象的底层操作,可以被 Proxy 拦截器调用。Reflect 对象的方法和 Proxy 拦截器的方法是一一对应的。
js
// 创建一个简单的代理
let target = { value: 42 };
let handler = {
get: function (target, prop, receiver) {
console.log(`Getting ${prop}`);
return Reflect.get(target, prop, receiver);
},
set: function (target, prop, value, receiver) {
console.log(`Setting ${prop} to ${value}`);
return Reflect.set(target, prop, value, receiver);
},
};
let proxy = new Proxy(target, handler);
proxy.value; // 获取 value,输出: Getting value
proxy.value = 100; // 设置 value 为 100,输出: Setting value to 100
应用
Vue3
在 Vue 3 中,Proxy 是一个关键的特性,用于实现响应式系统。Vue 3 的响应式系统在设计上使用了 Proxy 来劫持对象的访问和修改操作,从而实现了数据的响应式更新。
- 数据劫持: Vue 3 中通过使用 Proxy 对象,可以劫持数据对象的读取和修改操作。这允许 Vue 追踪对响应式对象的访问,并在数据发生变化时自动触发相应的更新。
- 依赖追踪: Vue 3 利用 Proxy 捕获数据的读取操作,从而建立起一个依赖图。每个数据的读取操作都会被记录为一个依赖,当数据发生变化时,依赖会被通知,触发更新。
- 观察者模式: Vue 3 的响应式系统中使用了观察者模式,Proxy 对象被用作观察者,负责观察被劫持的数据对象。当数据变化时,观察者会通知相关的订阅者执行更新操作。
设计原理
- Proxy 代理: Vue 3 中使用 Proxy 对象来代理数据对象。Proxy 对象允许拦截对象的底层操作,例如读取和修改属性。
- Reflect 反射: Vue 3 在 Proxy 拦截器中广泛使用了 Reflect 对象。Reflect 对象提供了一个与 Proxy 拦截器一一对应的方法,用于执行默认操作。
- 依赖追踪: Vue 3 使用了一个全局的响应式状态管理对象,称为
ReactiveEffect
,用于跟踪正在执行的响应式函数以及当前正在访问的依赖项。 - 响应式函数: 当访问一个响应式对象的属性时,Vue 3 会创建一个响应式函数,并将该函数与正在执行的响应式函数进行关联。这样就建立了一个依赖关系,当数据变化时,相关的响应式函数会被触发。
- 批量更新: 为了提高性能,Vue 3 中引入了批量更新的概念。即使数据发生多次变化,Vue 3 会在下一个微任务中批量执行更新,以减少不必要的计算和渲染操作。
TS
// packages/reactivity/src/reactive.ts#L241-L278
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>,
) {
// 判断目标对象是否为对象
// 确保只有对象才能被转换成响应式对象。
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
// 如果目标对象已经具有代理对象,并且不是只读的响应式对象,直接返回目标对象。
// 这是为了避免重复创建代理对象。
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
// 如果 `proxyMap` 中已经有了目标对象到代理对象的映射关系,直接返回已有的代理对象。
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
// 使用 `getTargetType` 函数判断目标对象的类型,
// 如果是无效类型,直接返回目标对象。
// 这里的类型判断主要用于确定使用哪种代理处理器。
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 使用 `Proxy` 构造函数创建代理对象,
// 根据目标对象的类型选择相应的代理处理器。
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
)
// 将目标对象与创建的代理对象进行映射,以便后续直接返回已有的代理对象。
proxyMap.set(target, proxy)
// 返回创建的代理对象。
return proxy
}
TS
// packages/reactivity/src/baseHandlers.ts#L89-L237
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
protected readonly _isReadonly = false,
protected readonly _shallow = false,
) {}
// `get` 方法用于拦截目标对象的属性访问操作。
// 根据属性名和当前的代理对象,进行不同的处理,
// 包括标识是否是只读、是否是浅层、是否是数组等情况。
// 还涉及到对属性值的追踪(`track`)和返回新的代理对象。
get(target: Target, key: string | symbol, receiver: object) {
const isReadonly = this._isReadonly,
shallow = this._shallow
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (key === ReactiveFlags.RAW) {
if (
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target) ||
// receiver is not the reactive proxy, but has the same prototype
// this means the reciever is a user proxy of the reactive proxy
Object.getPrototypeOf(target) === Object.getPrototypeOf(receiver)
) {
return target
}
// early return undefined
return
}
const targetIsArray = isArray(target)
if (!isReadonly) {
// 如果目标对象是数组并且 `key` 是数组相关的内置方法,
// 则使用 `Reflect.get` 获取相应的内置方法。
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
const res = Reflect.get(target, key, receiver)
// 如果 `key` 是特定的 Symbol 或不可追踪的键,
// 则直接返回目标对象上的属性值。
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
// 如果 `key` 对应的值是对象,将其转换为相应的响应式对象(只读或可变),然后返回。
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
class MutableReactiveHandler extends BaseReactiveHandler {
// 构造函数接收一个可选参数 `shallow`,用于标识是否是浅层响应式对象。
// 调用父类 `BaseReactiveHandler` 的构造函数,
// 并将 `_isReadonly` 设置为 `false`,
// `_shallow` 设置为传入的 `shallow` 值。
constructor(shallow = false) {
super(false, shallow)
}
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object,
): boolean {
// 获取目标对象上 `key` 对应的旧值 `oldValue`。
let oldValue = (target as any)[key]
// 如果不是浅层响应式且新旧值都不是只读对象,并且值有变化,
// 将新旧值都转换为原始值(去除响应式包装)。
if (!this._shallow) {
const isOldValueReadonly = isReadonly(oldValue)
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
// 如果目标对象不是数组且 `key` 对应的旧值是 Ref 对象而新值不是 Ref 对象,
// 将 Ref 对象的值修改为新值。
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
if (isOldValueReadonly) {
return false
} else {
oldValue.value = value
return true
}
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
// `deleteProperty` 方法用于拦截目标对象的属性删除操作
deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
// `has` 方法用于拦截目标对象的 `in` 操作符。
has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
// `ownKeys` 方法用于拦截目标对象的 `Object.keys`、`Object.getOwnPropertyNames` 等操作。
ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE,
isArray(target) ? 'length' : ITERATE_KEY,
)
return Reflect.ownKeys(target)
}
}
其他
1. 访问控制
通过代理,你可以实现对对象属性的访问控制,例如只读或只写属性:
js
let person = { name: 'John', age: 30 };
let readOnlyPerson = new Proxy(person, {
get: function (target, prop) {
console.log(`Accessing ${prop}`);
return Reflect.get(target, prop);
},
set: function (target, prop, value) {
console.log(`Setting ${prop} is not allowed`);
return false; // 不允许设置属性值
},
});
readOnlyPerson.name; // 访问 name 属性,输出: Accessing name
readOnlyPerson.age = 31; // 尝试设置 age 属性,输出: Setting age is not allowed
2. 数据验证
使用代理来实现数据验证,确保只有符合条件的数据可以被设置:
js
let user = { username: 'john_doe', password: 'secret123' };
let secureUser = new Proxy(user, {
set: function (target, prop, value) {
if (prop === 'password' && typeof value !== 'string') {
console.log('Invalid password format');
return false;
}
return Reflect.set(target, prop, value);
},
});
secureUser.password = 'newPassword'; // 设置密码,有效
secureUser.password = 123; // 设置无效,输出: Invalid password format
3. 缓存代理
通过代理实现缓存,可以在访问某个值时检查缓存是否已有该值,避免重复计算:
js
function expensiveOperation() {
// 模拟耗时计算
console.log('Performing expensive operation');
return Math.random();
}
let cachedValue = null;
let cachedProxy = new Proxy({}, {
get: function (target, prop) {
if (prop === 'value') {
if (!cachedValue) {
cachedValue = expensiveOperation();
}
return cachedValue;
}
},
});
console.log(cachedProxy.value); // 第一次调用,输出: Performing expensive operation
console.log(cachedProxy.value); // 第二次调用,直接使用缓存值
4. 日志记录
使用代理记录对象属性的访问和修改操作,用于调试或日志记录:
js
let loggedObject = new Proxy({}, {
get: function (target, prop) {
console.log(`Getting property ${prop}`);
return Reflect.get(target, prop);
},
set: function (target, prop, value) {
console.log(`Setting property ${prop} to ${value}`);
return Reflect.set(target, prop, value);
},
});
loggedObject.name = 'John'; // 设置属性,输出: Setting property name to John
console.log(loggedObject.name); // 获取属性,输出: Getting property name
等其他应用场景......
未完待续...
参考资料
《JavaScript 高级程序设计》(第 4 版)