用 Proxy 实现反应式计算
这是 js-gym 上的一道题。题目链接https://github.com/hayeah/js-gym/blob/master/problems/04.md
下面是问题描述:
MobX 是一个反应式数据框架。
反应式计算就像 Excel 表单。数据 B 依赖于数据 A,如果 A 改变了,那么 B 应该自动更新。
一个简单的例子如下:
const { observable, autorun } = require('mobx');
let counter = observable({
value: 0,
});
// 每次 counter.value 有改动,autorun 的回调会自动运行
autorun(() => {
console.log("counter:", counter.value);
});
setInterval(() => {
// 改动 counter.value 会触发 autorun 的回调
counter.value += 1;
}, 1000);
/*
输出
counter: 0
counter: 1
counter: 2
counter: 3
counter: 4
counter: 5
...
*/
autorun
在运行回调的时候会去分析哪些对象的哪些属性有被读取。当任何数据依赖有改动,autorun
会再次去运行回调。
下面这个例子演示多个可观察的对象,和多个 autorun:
// 创建两个可观察的对象
let o = observable({
a: 1,
});
let o2 = observable({
b: 2,
});
// 可以直接读取对象的属性
console.log(o.a);
// 1
console.log(o.b);
// 2
// autorun 会先运行一次回调,分析可观察对象那些属性有被引用。
// o 是个可观察对象,每次 o.a 更改都要重新运行一次回调。
autorun(() => {
// 回调 1 - o.a 有改动
console.log(`a => ${o.a}`)
});
// o2 是个可观察对象,每次 o2.b 更改都要重新运行一次回调。
autorun(() => {
// 回调 2 - o2.b 有改动
console.log(`b => ${o2.b}`)
});
// 每次 o.a 或者 o2.b 更改都要重新运行一次回调。
autorun(() => {
// 回调 3 - o.a 或者 o2.b 有改动
console.log(`a + b => ${o.a + o2.b}`)
});
// 会触发回调 1, 2
o.a = 100;
// 会触发回调 2,3
o2.b = 200;
/*
输出:
1
2
a => 1
b => 2
a + b => 3
a => 100
a + b => 102
b => 200
a + b => 300
*/
请利用 ES6 Proxy 来实现 autorun
和 observable
。
observable 只需要支持对象,不需要支持嵌套对象。
参考资料
####下面是我的答案
简单分析下原理,通过 Proxy
的 get
和 set
方法,对原有对象进行拦截。当触发对象 get 操作时,通过判断是不是由 autorun
函数添加回调时引起的。如果是,则将回调函数添加到一个订阅器中。当触发对象 set
操作时,执行订阅器中的回调。
function observable (obj) {
let map = {} // 保存不同 key 的订阅器
let handler = {
get (target, key, receiver) {
if (autorun.cb) {
if (map[key] == null) {
map[key] = []
}
// 将回调函数添加到订阅器中
map[key].push(autorun.cb)
}
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
if (target[key] === value) {
return
}
Reflect.set(target, key, value, receiver)
if (map[key]) {
// 执行订阅器中的回调
map[key].forEach(cb => cb())
}
}
}
return new Proxy(obj, handler)
}
function autorun (cb) {
autorun.cb = cb
/* 调用 cb 函数,cb 函数会触发对象的 get 操作,
由于此时 autorun.cb === cb, 所以触发 get 时,会将 cb 添加到相应的订阅器中
*/
cb()
/* 将 autorun.cb 赋值为 null。 当触发对象 get 操作时,当做普通操作
*/
autorun.cb = null
}
autorun.cb = null
机智的你,是不是想到用 Object.defineProperty
也能实现呢😂