基于 rx.js 创建的响应式对象,让对象之间的互操作跨越时间和空间范畴

本文是我在编写 Rdeco 相关文档的一部分,其实关于这个库,我一直比较纠结他的意义,这个意义并不是指库本身的价值,而是这个库的定位,这个库最早的定位只是为了摆脱 React 复杂模式下的状态管理生态的繁重,最初只是封装了 useReducer 这样的能力,其雏形是 structure-react-hook, 如果你读过我几个月前的文章可能会有些印象,不过随着时间推移,我越来越发现这个库所具备的能力已经早已超过了最初的预期,在加入了 rx.js 之后,我发现这可能是一种全新的编程范式。我个人使用过类似函数式, OOP, AOP 等不同的编程范式,rx.js 提倡的响应式是一种非常有益的编程范式,但因为 rx.js 的响应式基于函数式的,而函数式对于真实世界的建模过程式数学化的,是一种很晦涩但是非常巧妙地方式,也很难被掌握,为此我最终重新设计了 rdeco 这个库,把 rx.js 的响应式引入到普通的对象上,于是就有了响应式对象这个玩意。

响应式对象和普通对象

普通对象

const helloWorld = {
  text: 'hello world',
  print() {
    console.log(this.text);
  },
};

const code = {
  type: 'javascript',
  getType() {
    console.log(this.type);
  },
};
复制代码

普通对象的特征就是抽象具有内聚性,在对象的内部你可以创建属性和方法来模拟真实世界的其他事物。但是真实世界事物之间是有沟通的,可能是单向,也可能是双向,而普通对象并不具备通信能力,当两个对象需要沟通的时候,往往需要建立一种依赖关系,在相同的环境中才能执行。比如上述例子

code.callHelloWroldPrint = () => {
  helloWorld.print();
};
复制代码

code 对象如果要通知 helloWorld 对象去执行 print 方法 就需要获取到 helloWorld 对象的实例并对其直接进行操作。这种依赖关系让对象之间不得不建立起一个网状关系。并且这种网状关系是不可分割的,切不能被时间和空间所切分,这导致基于普通对象的建模方法很难真正实现对现实世界的抽象。

因为虽然现实世界中的各个对象之间的关系也是网状关系,但是我们的网状关系是可以切分到不同的时间和空间中去的,这就让对象之间的互操作变得不那么强调时间和空间的一致性。

举个最简单的例子,你通知你朋友在某天去参加一个活动,并不需要把你的朋友叫到跟前当面嘱咐他,这是因为我们拥有各种通信工具(电话、信件……)和通信标识(家庭住址,名字,地址,邮编……)

而编程本质上是将现实世界虚拟化的一个过程,为了能够做到这一点,我们需要一种能够模拟现实世界的这种通信机制的方式来让普通对象能够在不同的时间和空间上互操作。

而这正是 @rdeco/core 的意义。

rdeco 为普通对象添加了比订阅发布更强大的响应式机制, 这种机制可以让普通对象在不同的时间和空间上互操作。

响应式对象

让我们创建两个分布在不同空间,不同时间加载的对象,通常不同空间是指发布到不同的资源服务器上,例如我们将 jacky 对象发布到 cdn.a/jacky.js

import { create } from 'rdeco';

create({
  name: 'jacky',
  state: {
    age: 19,
  },
  exports: {
    setAge(newAge) {
      this.controller.setAge(newAge);
    },
    getAge(next) {
      next(this.state.age);
    },
  },
  controller: {
    setAge() {
      this.setter.age(20);
    },
  },
});
复制代码

这个 jacky 对象暴露了 2 个方法可供其他对象调用, 同时在调用 printAge 的时候会设置自己的 age 从 19 → 20 。

然后我们再创建另一个对象 ann, 并将其发布到 cdn.b/ann.js

import { create, inject } from 'rdeco';

create({
  name: 'ann',
  subscribe: {
    jacky: {
      state: {
        age({ nextState }) {
          console.log(nextState);
        },
      },
    },
  },
  controller: {
    onStart() {
      inject('jacky')
        .getAge()
        .then((age) => {
          inject('jacky').setAge(age + 1);
        });
    },
  },
});
复制代码

对象 ann 在初始化的时候调用了 jacky 对象的 getAge 方法获取到当前 jacky 的 age,同时 + 1 后调用 jacky 的 setAge 方法更新 jacky 的 age,我们将其类比成现实世界的模型。

ann 问 jacky 多大了? jacky 回答 19, ann 说那你得虚岁应该是 20

如果这件事发生在同一空间和时间下,比如 ann 和 jacky 面对面在一个下午偶遇闲聊了下, 那么你使用普通对象就能抽象这件个过程。 但如果 ann 和 jacky 是两个国家的人,并且时区不同, ann 早上发消息问 jacky, jacky 此时还在睡觉,等 jacky 回复的时候 ann 又睡觉了。 那么普通对象就无法抽象这个过程,因为两者的空间和时间并不相同。

反面来讲,这种时间和空间上的无法分割也导致了,前端 JavaScript 代码只能堆积却不能拆分的原因。因为我们编写的模块,npm 里的包必须被 download 到本地然后在同一时间和空间中运行才能正常互操作。

只有极少数环境级的模块能被放到 cdn 上,打破空间上的依赖,但即便如此,你并不能在 react 加载之前运行 react.createElement 对么。 这依然是一种时间上的依赖。

Rdeco 具备一组 API 让响应式对象的互操作可以跨越时间和空间的限制,下面的内容可能过于文档化,不过因为你从未见过这份文档,因此我想应该不会过于枯燥。

API

响应式对象包含一组互操作的 API,你通过这组 API 可以让两个响应式对象摆脱对时间和空间的依赖进行互操作

exports

exports 用来暴露可以被 inject 后调用的方法。

inject('mdoule-a').foo();

create({
  name: 'module-a',
  exports: {
    foo() {
      console.log('foo');
    },
  },
});

// log foo
复制代码

通过 next 返回调用结果

如果你需要在 exports 暴露的方法中传递一些值给调用方,可以使用 next, next 函数在所有 exports 暴露的方法中都存在

inject('mdoule-a')
  .foo()
  .then((foo) => {
    console.log(foo);
  });

create({
  name: 'module-a',
  exports: {
    foo(next) {
      next('foo');
    },
  },
});
// log foo
复制代码

inject

inject 用来给目标模块发送指令,这个过程并不需要目标模块真实就绪,就好比你给对方写信,并不需要知道对方在不在家。

  • inject([moduleName]) => exports

subscribe

subscribe 用来响应目标对象的一系列操作

  • subscribe.state[key]({nextState, prevState, state})
create({
  name: 'foo',
  subscribe: {
    bar: {
      state: {
        name({ nextState }) {
          console.log(nextState);
        },
      },
    },
  },
});

const bar = create({
  name: 'bar',
  state: {
    name: null,
  },
  controller: {
    onNameSet(name) {
      this.setter.name(name);
    },
  },
});

bar.controller.onNameSet('foo');

//console.log foo
复制代码
  • subscribe.eventkey

event 和 emit api 是一组关系 api,目标对象可以通过 emit 让其他对象能够响应对应的 event 函数

create({
  name: 'foo',
  subscribe: {
    event: {
      nameSetOver(name) {
        console.log(`hello ${name}`);
      },
    },
  },
});

const bar = craete({
  name: 'bar',
  state: {
    name: null,
  },
  controller: {
    onStart() {
      this.setter.name('bar');
      this.emit('nameSetOver', 'bar');
    },
  },
});
bar.conroller.onStart();

// console.log hello bar
复制代码
  • subscirbe.controllerkey
  • subscirbe.servicekey

controller 和 service 只是一组 event 的快捷语法,避免你过多的声明类似的 emit 事件函数

为什么 event 不能使用 next 来返回值?

和 exports 不同 subscribe 本质是一种广播模式,如果提供 next 返回值会导致一些意想不到的情况发生。所以如果你需要在响应 event 之后返回结果给调用对象,应该通过 exports 暴露的方法。 虽然这样可能会有点绕, 但对于跨越了时间和空间的通信来说,收发的准确性和流向的可控性更为重要

最后依然是例行宣传,如果你对响应式对象感兴趣,或者对 rx.js 感兴趣可以关注我们的项目 github.com/kinop112365…

猜你喜欢

转载自juejin.im/post/7036259575623843847