作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.< / div >< / div >
马克•埃文斯
< / div >

马克•埃文斯

< / div >
验证专家 在工程< / div >< / div >
13 的经验< / div >< / div >< / div >< / div >

Mark是一名全栈软件工程师,曾为包括英特尔在内的主要服务公司编写应用程序, 迪斯尼, 天空, 和沃达丰. 他带领反应和RxJS团队从原型开发到商业发布, 拥有牛津大学和国王学院物理学硕士学位.

< / div >< / div >

专业知识

RxJS反应.jsJavaScript< / div >< / div >

以前在

天空< / div >< / div >< / div >< / div >< / div >< / div >< / div >< / div >< / div >
< / div >
< / div >
< / div >< / div >分享< / div >< / div >< / div >
< div >

并不是所有的前端开发人员都有相同的观点 RxJS. 在这个范围的一端是那些不知道或者很难使用RxJS的人. 另一端是许多开发人员(特别是 角的工程师),他们经常成功地使用RxJS.

RxJS可以用于 状态管理 与任何前端框架以惊人的简单和强大的方式. 本教程将介绍一个RxJS/反应 方法,但是所展示的技术可以转移到其他框架.

一个警告:RxJS可能很冗长. 为了解决这个问题,我组装了一个实用程序库来提供一个简写,但我也将解释这个实用程序库是如何使用RxJS的,以便纯粹主义者可以选择较长的, 自用的路径.

多应用案例研究

在一个主要的客户项目中,我和我的团队写了几个 打印稿 应用程序 使用的反应 这些额外的库:

  • StencilJS:用于编写自定义web元素的框架
  • LightningJS一个基于webgl的框架,用于编写动画应用程序
  • ThreeJS:用于编写3D WebGL应用程序的JavaScript库

因为我们在应用中使用了类似的状态逻辑, 我觉得这个项目将受益于一个更健壮的状态管理解决方案. 具体来说,我认为我们需要一个解决方案:

  • Framework-agnostic.
  • 可重用的.
  • 打印稿-compatible.
  • 简单易懂.
  • 可扩展的.

基于这些需求,我探索了各种选择,以找到最适合的.

状态管理解决方案选项

我排除了以下候选解决方案, 基于它们与我们的要求相关的各种属性:

<的ad>

候选人

值得注意的属性

拒绝理由

回来的

  • Widely used; effective in providing structure to 状态管理.
  • 建立在 榆树架构,证明它适用于单页应用程序.
  • 要求开发人员使用不可变数据.
  • 沉重而复杂.
  • 需要大量的样板代码.
  • 由于其减速器(如.g.(动作、动作创建者、选择器等)都连接到一个中央存储中.

Vuex

  • 使用单个中央存储.
  • 提供了一个 模块 能够很好地实现状态逻辑重用的机制.
  • 主要用于VueJS应用程序.

MobX

  • 提供可重用的存储类.
  • 减少样板和复杂性问题.

  • 通过大量使用代理对象来隐藏其实现魔法.
  • 挑战重用纯表示组件, 因为它们必须被包装才能成为mobx感知.
< / div >

当我复习的时候 RxJS 并注意到它的算子集合, 可见, 和主题, 我意识到它符合所有条件. 用RxJS构建可重用状态管理解决方案的基础, 我只需要提供一个实用程序代码的薄层,以便更流畅地实现.

RxJS简介

RxJS自2011年以来一直存在,并被广泛使用, 它本身和作为基础的一些其他 库,比如Angular.

RxJS中最重要的概念是 可观测的,它是一个可以随时发出值的对象,订阅者跟随更新. 正如介绍的那样 承诺 对象标准化 异步回调模式 转化为对象后,可观察对象标准化了 观察者模式.

注意:在本文中,我将采用在可观察对象后面加上a的惯例 $ 符号,所以一个变量像 元数据 意味着它是。 可观测的.

//一个简单的可观察的例子
从“rxjs”中导入{interval};

Const 秒美元 = interval(1000); // 秒美元 is an 可观测的

秒美元.订阅((n) => 控制台.日志(' ${n + 1}秒已经过去了!`));

//控制台日志:
// "1秒过去了!"
// "2秒过去了!"
// "3秒过去了!"
// ...

特别地,一个可观察对象可以是 管道 通过操作员, 哪一个可以改变发出的值, 发出事件的时间/数量, 或两个.

//一个带有操作符的可观察对象示例
从“rxjs”中导入{interval, map};

const secsSquared美元 = interval(1000).管(map(s => s*s));

secsSquared美元.订阅(控制台.日志);

//控制台日志:
// 0
// 1
// 4
// 9
// ...

可观察对象有各种形状和大小. 例如,在时间方面,他们可以:

  • 在未来的某个时刻发出一次,就像一个承诺.
  • 将来会发出多次,比如用户单击事件.
  • 一旦它们被订阅,就会发出一次,就像在平凡中一样 of 函数.
//触发一次
const 元数据 = fromF等h("http://api . data ".鸡蛋.com/鸡蛋?type =油炸");

//触发多次
const click $ = fromEvent(document, "click");

//订阅时触发一次
Const $ = of(4);
四美元.订阅((n) => 控制台.log(n)); // logs 4 immediately

所发出的事件对每个订阅者可能是相同的,也可能不是相同的. 可观测对象通常被认为是冷可观测对象或热可观测对象. Cold 可见 operate like people streaming a show on Netflix who watch it in 的ir own time; each observer 得到s 的ir own 集 of events:

//冷可观察的例子
Const 秒美元 = interval(1000);

/ /爱丽丝
秒美元.订阅((n) => 控制台.log(' Alice: ${n + 1} '));

// Bob在5秒后订阅
集Timeout(() =>
  秒美元.订阅((n) => 控制台.log(' Bob: ${n + 1} '))
, 5000);

/*控制台再次从1开始为Bob */
// ...
//“Alice: 6”
//“Bob: 1”
//“爱丽丝:7”
// "Bob: 2"
// ...

Hot 可见 函数 like people watching a live football match who all see 的 same thing at 的 same time; each observer 得到s events at 的 same time:

//热可观察的例子
const shareseconds $ = interval(1000).管(分享());

/ /爱丽丝
sharedSeconds美元.订阅((n) => 控制台.log(' Alice: ${n + 1} '));

// Bob在5秒后订阅
集Timeout(() =>
  sharedSeconds美元.订阅((n) => 控制台.log(' Bob: ${n + 1} '))
, 5000);

/* Bob现在看到与Alice相同的事件*/
// ...

//“Alice: 6”
// "Bob: 6"
//“爱丽丝:7”
//“Bob: 7”
// ...

你可以用RxJS做更多的事情, 公平地说,新手可能会对观察者等功能的复杂性感到有些困惑, 运营商, 主题, 和调度器, 以及多播, 单播, 有限的, 无限的可观测对象.

值得庆幸的是, 状态管理实际上只需要有状态的可观察对象(rxjs的一小部分), 我接下来会解释.

RxJS有状态可观察对象

我所说的有状态可观察对象是什么意思?

首先,这些可观察对象具有当前值的概念. 具体来说,订阅者将同步获取值,甚至在运行下一行代码之前:

//假设美元的名字的当前值为"Fred"

控制台.日志(“订阅”);
美元的名字.订阅(控制台.日志);
控制台.日志(“订阅”后);

/ /日志:
//“订阅前”
/ /“弗雷德”
//“订阅后”

其次,有状态的可观察对象在每次值改变时都会触发一个事件. 此外,它们很热门,这意味着所有订阅者在同一时间看到相同的事件.

持有状态 BehaviorSubject 可观测的

RxJS的 BehaviorSubject 是一个有状态的可观察对象与上述属性. 的 BehaviorSubject 可观测的包装一个值,并在每次值改变时发出一个事件(以新值作为负载):

const numPieces美元 = new BehaviorSubject(8);

numPieces美元.订阅((n) => 控制台.Log (' ${n}块蛋糕的剩余'));
// "剩下8块蛋糕"

/ /后……
numPieces美元.next(2); // next(...)设置/发出新值
// "剩下2块蛋糕"

这似乎正是我们实际保持状态所需要的, 这段代码可以处理任何数据类型. 为了将代码定制为单页应用程序,我们可以利用RxJS操作符使其更高效.

提高效率 distinctUntilChanged 操作符

在处理状态时, 我们希望可观察对象只发出不同的值, 所以如果相同的值被设置多次并且重复, 只发出第一个值. 这对于单页面应用程序的性能非常重要,并且可以使用 distinctUntilChanged 接线员:

const rugbyScore美元 = new BehaviorSubject(22),
  distinctScore美元 = rugbyScore美元.管(distinctUntilChanged ());

distinctScore美元.订阅((score) => 控制台.log('分数是${score} '));

rugbyScore美元.next(22); // distinctScore美元 does not emit
rugbyScore美元.next(27); // distinctScore美元 emits 27
rugbyScore美元.next(27); // distinctScore美元 does not emit
rugbyScore美元.next(30); // distinctScore美元 emits 30

/ /日志:
//“分数是22”
//“分数是27”
//“分数是30”

的结合 BehaviorSubjectdistinctUntilChanged 实现保持状态的大部分功能. 接下来我们要解决的是如何处理衍生态.

的派生状态 结合Latest 函数

在单页应用中,派生状态是状态管理的重要组成部分. This type of state is derived from o的r pieces of state; for example, 全名可能由名和姓派生而来.

在RxJS中,可以使用 结合Latest 函数,连同… map 操作符:

const firstName$ = new BehaviorSubject(“Jackie”),
  lastName美元 = new BehaviorSubject("Kennedy"),
  fullName美元 = 结合Latest([firstName$, lastName美元]).管(
    map(([first, last]) => `${first} ${last}`)
  );

fullName美元.订阅(控制台.日志);
//日志"Jackie Kennedy"

lastName美元.下一个(“奥纳西斯”);
//日志“Jackie Onassis”

但是,计算派生状态(内部的部分) map 函数)可能是一个昂贵的操作. 而不是为每个观察者计算, 如果我们能表演一次就更好了, 并缓存结果以便在观察者之间共享.

这很容易通过管道完成 shareReplay 操作符. 我们还会使用 distinctUntilChanged 同样,如果计算的状态没有改变,观察者不会收到通知:

const num1$ = new BehaviorSubject(234),
  num2美元 = new BehaviorSubject(52),
  由于美元 = 结合llatest ([num1$, num2美元]).管(
    map(([num1, num2]) => someExpensiveComputation(num1, num2)),
    shareReplay (),
    distinctUntilChanged ()
  );

由于美元.订阅((result) => 控制台.log("Alice看到",result));
//计算结果
//记录“Alice看到9238”

由于美元.订阅((result) => 控制台.log("Bob看到",result));
//使用缓存结果
//记录“Bob看到9238”

num2美元.下一个(53);
//只计算一次
//记录“Alice看到11823”
//记录“Bob看到11823”

我们已经看到了 BehaviorSubject 管道穿过 distinctUntilChanged 操作符在保持状态方面工作良好 结合Latest管道通过 map, shareReplay, distinctUntilChanged,可以很好地管理派生状态.

然而, 随着项目范围的扩大,编写这些相同的可观察对象和操作符组合是很麻烦的, 因此,我编写了一个小库,为这些概念提供了简洁方便的包装.

rx-state 方便图书馆

我没有每次都重复相同的RxJS代码,而是编写了一个小的、免费的 方便图书馆, rx-state,它提供了上面提到的RxJS对象的包装器.

而RxJS的可观察对象是有限的,因为它们必须与无状态的可观察对象共享一个接口, rx-state 提供方便的方法,如得到ter, 现在我们只对有状态的可观察对象感兴趣了,它们变得有用了吗.

图书馆围绕着两个物体旋转 原子,表示保持状态 结合 函数,用于处理派生状态:

<的ad>

概念

RxJs

rx-state

持有国家

BehaviorSubjectdistinctUntilChanged

原子

派生的状态

结合Latest, map, shareReplay, distinctUntilChanged

结合

< / div >

原子可以被看作是任何状态(字符串)的包装器, 数量, 布尔, 数组, object, 等.),这使得它可以观察到. 其主要方法有 得到, , 订阅,它可以与RxJS无缝协作.

const 美元一天 = 原子("Tuesday");

美元一天.订阅(day => 控制台.“醒醒,现在是${day}”!`));
//日志“醒醒,今天是星期二!"

美元一天.得到() // —> "Tuesday"
美元一天.集(周三)
//日志“醒醒,今天是星期三!"
美元一天.得到() // —> "Wednesday"

完整的API可以在 GitHub库.

类创建的派生状态 结合 函数从外部看起来就像一个原子(实际上,它是一个只读原子):

Const id$ = 原子(77),
  allUsers$ = 原子({
    42: {name: "Rosalind Franklin"},
    77:{名字:“居里夫人”}
  });

const 用户美元 = 结合([allUsers$, id$], ([users, id]) => users[id]);

//当用户$发生变化时,执行一些操作.e.、控制台.日志).
用户美元.订阅(user => 控制台.用户是${User . log.名称}'));
//记录“用户是居里夫人”
用户美元.得到() // —> "Marie Curie"

id$.集(42)
//记录"User is Rosalind Franklin"
用户美元.得到() // —> "Rosalind Franklin"

注意,原子从 结合 没有 方法,因为它是从其他原子(或RxJS可观察对象)派生的。. 的完整API 结合 可以在 GitHub库.

现在我们有一个简单的, 处理状态的有效方式, 我们的下一步是创建可重用的逻辑 不同的应用和框架.

最棒的是我们不需要更多的库, 因为我们可以使用老式的JavaScript类轻松封装可重用逻辑, 创建商店.

可重用的JavaScript存储

没有必要引入更多的库代码来处理将状态逻辑封装到可重用块中的问题, 作为一个普通的JavaScript类就足够了. (如果您喜欢更实用的封装逻辑的方式, 这些应该同样容易实现, 给定相同的构建模块: 原子结合.)

状态可以作为实例属性公开公开, 状态的更新可以通过公共方法完成. 举个例子, 假设我们想要在2D游戏中跟踪玩家的位置, 有x坐标和y坐标. 此外,我们想知道玩家从原点(0,0)移动了多远:

从“@hungry-egg/rx-state”中导入{原子, 结合};

//我们的播放器商店
类Player {
  //(0,0)是"bottom-left". 标准笛卡尔坐标系
  X $ = 原子(0);
  Y $ = 原子(0);
  // x$ 和 y$ are being observed; when those change, 的n update 的 distance
  //注意:我们使用勾股定理进行计算
  距离$ =组合([这 ..x美元,这.y$], ([x, y]) => Math.根号(x * x + y * y);

  moveRight () {
    这.x$.update(x => x + 1);
  }

  moveLeft () {
    这.x$.update(x => x - 1);
  }

  moveUp () {
    这.y$.update(y => y + 1);
  }

  moveDown () {
    这.y$.update(y => y - 1);
  }
}

//实例化一个store
const 球员 = new 球员 ();

球员.距离美元.订阅(d => 控制台.log('玩家${d}m偏离'));
//记录“玩家距离0米”
球员.moveDown ();
//记录“玩家距离1米”
球员.moveLeft ();
//记录“玩家为1”.4142135623730951米”

因为这只是一个普通的JavaScript类,所以我们可以使用 私人公共 关键字的方式,我们通常会暴露我们想要的接口. (打印稿提供了这些关键字,现代JavaScript也有 私课功能.)

作为旁注,在某些情况下,您可能希望暴露的原子是只读的:

/ /允许
球员.x$.得到 ();

//订阅但不允许
球员.x$.(10);

对于这些情况, rx-state 提供了一个 有几个选择.

尽管我们所展示的内容相当简单,但我们现在已经介绍了状态管理的基础知识. 将我们的函数库与回来的等常见实现进行比较:

  • 在雷杜克斯有仓库的地方,我们用了原子.
  • 回来的处理派生状态的库,比如Reselect,我们用过 结合.
  • 回来的有动作和动作创建者,而我们只有JavaScript类方法.

更重要的是, 因为我们的商店是简单的JavaScript类,不需要任何其他机制来工作, 它们可以打包并跨不同的应用程序(甚至跨不同的框架)重用. 让我们探索一下如何在反应中使用它们.

反应集成

一个有状态的可观察对象可以很容易地用反应打开成一个原始值 useStateuseEffect 挂钩:

//获取任何“有状态可观察对象”当前值的方便方法
//行为学科目已经有得到Value方法,但这将不起作用
//导出状态
函数得到(可观察到的美元) {
  让价值;
  可观察到的美元.订阅((val) => (value = val)).退订();
  返回值;
}

//自定义反应钩子来展开可观察对象
函数useUnwrap(可观察到的美元) {
  const [value, 集Value] = useState(() => 得到(可观察到的美元));

  useEffect(() => {
    Const订阅= 可观察到的美元.订阅(集Value);
    返回函数cleanup() {
      订阅.退订();
    };
  },可见美元);

  返回值;
}

然后,使用上面的玩家例子,可以将可观察对象展开为原始值:

//“玩家”在现实中可能来自其他地方(例如.g.,另一个文件,或提供上下文)
const 球员 = new 球员 ();

MyComponent() {
  //将可观察对象展开为普通值
  const x = useUnwrap(播放器.x$),
    y =使用unwrap(播放器.y$);

  const h和leClickRight = () => {
    //通过调用方法更新状态
    球员.moveRight ();
  };

  回报(
    
玩家的位置是({x},{y})
); }

就像 rx-state 库中,我已经打包了 useWrap 钩, 以及一些额外的功能, 打印稿的支持, 还有一些额外的实用钩子 small rx-react 图书馆 GitHub上.

关于苗条集成的说明

苗条的 用户可能已经注意到原子和苗条商店之间的相似之处. 在本文中, 我将“存储”称为将原子构建块连接在一起的高级概念, 而苗条商店指的是构建模块本身, 和原子处于同一水平. 然而,原子和苗条存储仍然非常相似.

如果你只使用苗条的, 您可以使用苗条的存储而不是原子(除非您想通过RxJS操作符使用 方法). 实际上,苗条的有一个有用的内置特性:任何实现 特殊的合同 可以加上 $ 自动展开为原始值.

RxJS的可观察对象之后也会履行这个契约 支持更新. 我们的原子对象也是如此, 因此,我们的反应状态可以与苗条的一起使用,就好像它是一个苗条的存储一样,无需修改.

使用RxJS流畅的反应状态管理

RxJS拥有在JavaScript单页应用中管理状态所需的一切:

  • BehaviorSubjectdistinctUntilChanged 操作者为保持状态提供了良好的依据.
  • 结合Latest 函数,与 map, shareReplay, distinctUntilChanged 操作符,为管理派生状态提供了基础.

然而,手工使用这些操作符可能相当麻烦 rx-state的助手 原子 对象和 结合 函数. 通过将这些构建块封装在普通的JavaScript类中, 使用该语言已经提供的公共/私有功能, 我们可以构建可重用的状态逻辑.

最后,我们可以轻松地将平滑状态管理集成到 使用钩子进行反应rx-react 助手库. 与其他库集成通常会更简单,如苗条的示例所示.

可观测的未来

我预测以下几点更新将对未来的可观察对象最有用:

  • RxJS可观察对象的同步子集的特殊处理.e.,那些有当前价值概念的,两个例子是 BehaviorSubject 以及由此产生的可观察结果 结合Latest); for example, maybe 的y’d all implement 的 得到Value () 方法,和平常一样 订阅等等. BehaviorSubject 已经这样做了,但其他同步可观察对象没有.
  • 支持原生JavaScript可观察对象 现有的方案 等待进度.

这些变化将使不同类型的可观测物之间的区别更加清晰, 简化状态管理, 并带来更大的力量 JavaScript语言.

Toptal工程博客的编辑团队向 Baldeep辛格马丁Indzhov 查看本文中提供的代码示例和其他技术内容.

< / div >< / div >< / div >< / div >

关于总博客的进一步阅读:

< / div >

了解基本知识

  • 在反应中使用RxJS吗?

    是的,RxJS可以与许多JavaScript框架一起使用,包括反应. RxJS通常用于管理副作用,但也适合于管理状态.

    < / div >< / div >
  • RxJS在反应应用中流行吗?

    是的,但是在反应应用程序中不像回来的那样常见. 然而, RxJS为围绕公共前端库构建的应用程序中的状态管理提供了一种优雅且轻量级的替代方案.

    < / div >< / div >
  • 什么是反应中的状态管理?

    反应中的状态管理指的是在用户界面呈现时对数据的管理. 状态管理关注的是数据保存在哪里,以及如何将相关的数据更改从模型传递到用户界面.

    < / div >< / div >
  • 在反应中使用RxJS好吗?

    是的,RxJS功能强大,与反应配合得很好. RxJS为使用通用前端库构建的应用程序提供了轻量级的状态管理.

    < / div >< / div >
  • 为什么状态管理在反应中很重要?

    状态管理是前端应用程序的必要组成部分, 提供应用程序逻辑和用户界面之间的分离. 适当的状态管理使应用程序易于理解, 使用可重用的用户界面组件.

    < / div >< / div >
  • 什么是RxJS 可观测的?

    RxJS 可观测的是一个JavaScript对象,它可以随时向订阅者发送值和更新. 这些数据传输是可以观察到的, 在代码的其他部分与这些事件相一致的副作用可以被安排.

    < / div >< / div >
< / div >< / div >

标签

< / div >< / div >< / div >< / div >
聘请Toptal这方面的专家.< / div >现在雇佣< / div >< / div >
马克•埃文斯
< / div >

马克•埃文斯

验证专家 在工程< / div >< / div >
13 的经验< / div >< / div >

英国伦敦

2017年9月6日成为会员

< / div >< / div >< / div >

作者简介

Mark是一名全栈软件工程师,曾为包括英特尔在内的主要服务公司编写应用程序, 迪斯尼, 天空, 和沃达丰. 他带领反应和RxJS团队从原型开发到商业发布, 拥有牛津大学和国王学院物理学硕士学位.

< / div >< / div >
作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.< / div >

专业知识

RxJS反应.jsJavaScript< / div >< / div >

以前在

天空< / div >< / div >
雇用马克< / div >< / div >< / div >
< / div >< / div >
< / div >
< / div >
< / div >< / div >< / div >< / div >< / div >
< / div >< / div >
< / div >

世界级的文章,每周发一次.

< / div >

输入您的电子邮件,即表示您同意我们的 隐私政策.

< / div >< / div >< / div >< / div >< div >
< / div >

世界级的文章,每周发一次.

< / div >

输入您的电子邮件,即表示您同意我们的 隐私政策.

< / div >< / div >< / div >< / div >< / div >< / div >< / div >

Toptal开发者

< / div >< / div >

加入总冠军® 社区.

聘请开发人员 or 申请成为发展商< / div >< / div >< / div >