实现一个极简单的Mobx
- Published on
- Authors
- Name
- 0neSe7en
- @0ne_Se7en
先写个测试
最近写React的时候,使用 mobx
与 mobx-state-tree
作为状态管理。很喜欢 mobx
的使用方式。所以试着弄懂它背后的原理。
对于Mobx来说,最重要的是两个功能: observable
与 autorun
以及 autorun
对应的依赖收集功能。
下面的示例代码只是为了解释原理,有很多功能缺失和不完善的地方。
先写一个简单的单元测试,用来表示observable
与 autorun
的功能
const { observable, autorun } = require('../index');
it('should pass', (done) => {
let changed = false;
let shouldFailed = false;
const testObject = observable({ a: 10, b: 'test' });
autorun(() => {
console.log('autorun...', testObject.a);
expect(testObject.a).toBe(changed ? 11 : 10);
if (shouldFailed) { expect(shouldFailed).toBe(false) }
done()
});
changed = true;
testObject.a = 11;
shouldFailed = true;
testObject.b = '123';
})
- 把一个对象变成了可观察。这个对象有两个字段
a
和b
。 - 在
autorun
中,检测到函数只依赖.a
。 .a
被赋值的时候,执行autorun
里的函数。.b
被赋值,不会执行autorun
中的函数,因为.b
并没有被autorun
依赖。
依赖收集
- 需要知道
autorun(handler)
中,函数handler
都用到了observable
对象的哪些变量。 - 通过
observable
包装一个对象得到,使得对象中的字段被使用时(读取、赋值等)能够被触发一个函数。 - 当运行
handler
的时候,所有使用observable
对象的地方,都会被收集为依赖。 - 当
observable
对象的某个字段被赋值时,能够触发与之关联的所有autorun
函数。
实现
我们先实现observable
方法。完成上述提到的第二条。需要用到Proxy
功能。Proxy 是用于给set
get
的时候,加一个钩子。关于Proxy的具体使用,可以看一下MDN的文档。这里直接上代码+注释。
const proxyHandler = {
// 当尝试对target读取某个name字段时,会调用此方法
get(target, name) {
console.log('someone try to get...');
return target[name];
},
// 当尝试把target的name字段赋值为value时,会调用此方法
set(target, name, value) {
console.log('some try to set ...');
target[name] = value;
return true;
}
};
function observable(origin) {
return new Proxy(origin, proxyHandler);
}
接着实现上述提到的第三条。就是在运行handler
的时候,开始收集依赖。Mobx的做法是,给每个observable
对象,都加一个$mobx
字段。这里我们也采用这样的方法。
class Admin {
constructor() {
// 我为了简单,这里就采用 field -> handlers 的映射
this.dependsMap = new Map();
}
// 保存依赖...
save(field, handler) {
const depends = this.dependsMap.get(field) || new Set();
depends.add(handler);
this.dependsMap.set(field, depends);
}
// 当某个字段被修改时,调用此函数,触发依赖此字段的所有autorun handler
trigger(field) {
if (this.dependsMap.has(field)) {
const handlers = this.dependsMap.get(field);
for (const func of handlers) {
// 注意这里,在trigger里面,也需要收集handler的依赖
collecting = { func };
func();
collecting = null;
}
}
}
isDepend(field) {
return this.dependsMap.has(field);
}
}
function observable (object) {
Object.defineProperty(object, '$fake', {
enumerable: false, // 为了让 $fake 字段不能够被遍历出来
writeable: true,
configurable: true,
value: new Admin()
});
return new Proxy(object, proxyHandler);
}
最后,我们直接实现上述提到的第一步和第四步吧。
let collecting = null;
// autorun方法非常简单,就是在调用func之前,记录一下当前正在收集依赖即可。
function autorun (func) {
collecting = { func };
func();
collecting = null;
}
再完善一下proxyHandler
:
- 当
set
时,- 如果依赖收集开启中,则把
set
存入依赖收集中。注意这里有一个没处理的情况:我在handler1里修改了某个字段,应该让其它依赖此字段的handler都执行 - 如果依赖收集没有开启,则触发此字段的
handler
方法
- 如果依赖收集开启中,则把
- 当
get
时,- 如果开启依赖收集,则收集依赖后,返回结果
- 如果没开启,则直接返回此字段的值。
const objectProxyHandler = {
get(target, name) {
if (name === '$fake') { return target[name] }
if (collecting) {
target.$fake.save(name, collecting.func);
}
return target[name];
},
set(target, name, value) {
target[name] = value;
if (collecting) {
target.$fake.save(name, collecting.func);
} else {
if (target.$fake.isDepend(name)) {
target.$fake.trigger(name);
}
}
return true;
},
};
这样就实现了最简单的 observable
与 autorun
功能。
完整代码如下:
class Admin {
constructor() {
this.dependsMap = new Map();
}
save(field, handler) {
const depends = this.dependsMap.get(field) || new Set();
depends.add(handler);
this.dependsMap.set(field, depends);
}
trigger(field) {
if (this.dependsMap.has(field)) {
const handlers = this.dependsMap.get(field);
for (const func of handlers) {
collecting = { func };
func();
collecting = null;
}
}
}
isDepend(field) {
return this.dependsMap.has(field);
}
}
let collecting = null;
const objectProxyHandler = {
get(target, name) {
if (name === '$fake') {
return target[name];
}
if (collecting) {
target.$fake.save(name, collecting.func);
}
return target[name];
},
set(target, name, value) {
target[name] = value;
if (collecting) {
target.$fake.save(name, collecting.func);
} else {
if (target.$fake.isDepend(name)) {
target.$fake.trigger(name);
}
}
return true;
},
};
function observable (object) {
Object.defineProperty(object, '$fake', {
enumerable: false,
writeable: true,
configurable: true,
value: new Admin()
});
return new Proxy(object, objectProxyHandler);
}
function autorun (func) {
collecting = { func };
func();
collecting = null;
}
module.exports = {
observable,
autorun
};