在react逐步进入hooks的时代,mobx一如既往的简洁、易学和易用,有点时间的我最近开始啃mobx源码,打算出个最新版源码解析,这次就先出个proxy简单原理预热吧
1.前置api知识
当前的mobx版本在6.3+时代,各类api变化还是比较大的,并且因为装饰器提案的更改,mobx做了一次小范围的重构。从5版本开始,mobx就开始使用proxy的api来监听了,这里先简单介绍proxy
1.1 Proxy 代理
let proxy = new Proxy(target, handler)
复制代码
target
—— 是要包装的对象,可以是任何东西,包括函数。handler
—— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如get
捕捉器用于读取target
的属性,set
捕捉器用于写入target
的属性,等等。
对 proxy
进行操作,如果在 handler
中存在相应的捕捉器,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。
对于对象的大多数操作,JavaScript 规范中有一个所谓的“内部方法”,它描述了最底层的工作方式。例如 [[Get]]
,用于读取属性的内部方法,[[Set]]
,用于写入属性的内部方法,等等。这些方法仅在规范中使用,我们不能直接通过方法名调用它们。
对于每个内部方法,此表中都有一个捕捉器:可用于添加到 new Proxy
的 handler
参数中以拦截操作的方法名称:
内部方法 | Handler 方法 | 何时触发 |
---|---|---|
[[Get]] |
get |
读取属性 |
[[Set]] |
set |
写入属性 |
[[HasProperty]] |
has |
in 操作符 |
[[Delete]] |
deleteProperty |
delete 操作符 |
[[Call]] |
apply |
函数调用 |
[[Construct]] |
construct |
new 操作符 |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in , Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in , Object/keys/values/entries |
1.2 Reflect 反射
Reflect
是一个内建对象,就像是proxy的好兄弟,可用来简化 Proxy
的handler创建。
前面所讲过的内部方法,例如 [[Get]]
和 [[Set]]
等,都只是规范性的,不能直接调用。
Reflect
对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。
以下是执行相同操作和 Reflect
调用的示例:
操作 | Reflect 调用 |
内部方法 |
---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
实操一下:
let proxyObj = new Proxy(obj, {
get(target, propKey, receiver) {
console.log(`GET ${propKey}`);
return Reflect.get(target, propKey, receiver);
},
set(target, propKey, value, receiver) {
console.log(`SET ${propKey} = ${value}`);
return Reflect.set(target, propKey, value, receiver);
}
})
复制代码
2.相关设计模式
发布订阅模式和观察者模式比较相像,mobx内部实现原理比较复杂,更像是发布订阅模式,比观察者模式多了一个中介来通信。
在下面的模拟中我们用观察者模式来简化创建mobx observer的过程。
观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。
观察者模式有完成整个流程需要两个角色:
- 目标
- 观察者
简单流程如下:
目标<=>观察者,观察者观察目标(监听目标)-> 目标发生变化-> 目标主动通知观察者。
3.实现
这次我们简单实现一个核心api makeObservable 即以前的 @observable;
创建一个函数 makeObservable(target)
,该函数通过返回一个代理“使得对象可观察”。
// 示例
let user = {};
user = makeObservable(user); // 经过包装后可观察
user.observe((key, value) => { // 加上触发的方法
alert(`SET ${key}=${value}`);
});
user.name = "John"; // 改属性会触发 alerts: SET name=John
复制代码
3.1 解决方案
分析一下,makeObservable
返回的对象就像原始对象一样,但是具有 observe(handler)
方法,该方法可以将 handler
函数设置为在任何属性被更改时,都会被调用的函数。
每当有属性被更改时,都会使用属性的名称和属性值调用 handler(key, value)
函数。
解决方案包括两部分:
-
无论
.observe(handler)
何时被调用,我们都需要在某个地方记住 handler,以便以后可以调用它。我们可以使用 Symbol 作为属性键,将 handler 直接存储在对象中。 -
我们需要一个带有
set
捕捉器 的 proxy 来在发生任何变更时调用 handler。
3.2 实际代码
let handlers = Symbol('handlers'); // 取一个全局唯一属性,防止被覆盖
function makeObservable(target) {
// 1. 初始化 handler 存储
target[handlers] = [];
// 将 handler 函数存储到数组中,以便于之后调用
target.observe = function(handler) {
this[handlers].push(handler);
};
// 2. 创建一个 proxy 以处理更改
return new Proxy(target, {
set(target, property, value, receiver) {
let success = Reflect.set(...arguments); // 将操作转发给对象
if (success) { // 如果在设置属性时没有出现 error
// 调用所有 handler
target[handlers].forEach(handler => handler(property, value));
}
return success;
}
});
}
复制代码