用 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 来实现 autorunobservable

observable 只需要支持对象,不需要支持嵌套对象。

参考资料


下面是我的答案

简单分析下原理,通过 Proxygetset 方法,对原有对象进行拦截。当触发对象 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 也能实现呢😂

参考资料