简单实现MobX的Observable

在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.getOwnPropertyDescriptorfor..inObject.keys/values/entries
[[OwnPropertyKeys]] ownKeys Object.getOwnPropertyNamesObject.getOwnPropertySymbolsfor..inObject/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) 函数。

解决方案包括两部分:

  1. 无论 .observe(handler) 何时被调用,我们都需要在某个地方记住 handler,以便以后可以调用它。我们可以使用 Symbol 作为属性键,将 handler 直接存储在对象中。

  2. 我们需要一个带有 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;
    }
  });
}
复制代码

猜你喜欢

转载自juejin.im/post/7047451283610927135