这篇文章是由同行评审莫里茨克罗格 , 布鲁诺·莫塔和Vildan Softic 。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!
在深入探讨该主题之前,我们必须回答一个关键问题: 什么是反应式编程? 到目前为止,最流行的答案是反应式编程是使用并发数据流进行编程。 大多数时候,我们会发现并发一词已被异步替换,但是,稍后我们将看到流不必是异步的。
不难发现,“一切都是流”方法可以直接应用于我们的编程问题。 毕竟,CPU只不过是处理由指令和数据组成的信息流的设备。 我们的目标是观察该流并在有特定数据的情况下对其进行转换。
反应式编程的原理并不是JavaScript的全新概念。 我们已经有了诸如属性绑定, EventEmitter
模式或Node.js流之类的东西。 有时,这些方法的精致之处在于性能下降,抽象过于复杂或调试出现问题。 通常,与新抽象层的优点相比,这些缺点很小。 当然,我们的最少示例将不会反映通常的应用,而是尽可能简短。
事不宜迟,让我们动手使用JavaScript的反应式扩展 (RxJS)库。 RxJS使用链接很多,这是一种流行的技术,也用于jQuery之类的其他库中。 在SitePoint上可以找到方法链接的指南(在Ruby中)。
流示例
在深入研究RxJS之前,我们应该列出一些示例供以后使用。 一般而言,这也将结束对反应式编程和流的介绍。
通常,我们可以区分两种流:内部流和外部流。 尽管前者可以被认为是人为的并且在我们的控制范围内,但后者来自我们无法控制的来源。 外部流可能会从我们的代码中触发(直接或间接)。
通常,信息流不会等我们。 无论我们能否处理它们,它们都会发生。 例如,如果我们要在道路上观察汽车,我们将无法重新启动汽车流。 流的发生与我们是否观察到无关。 在Rx术语中,我们称其为可观察的热点 。 Rx还引入了冷可观测对象 ,其行为更像是标准迭代器,因此流中的信息由每个观察者的所有项目组成。
下图说明了一些外部流。 我们看到提到了(以前启动的)请求和通常设置的Web挂钩,以及诸如鼠标或键盘交互之类的UI事件。 最后,我们还可能从设备(例如GPS传感器,加速度计或其他传感器)接收数据。
该图像还包含一个标记为Messages的流。 消息可以几种形式出现。 最简单的形式之一是我们的网站与其他网站之间的通信。 其他示例包括与WebSocket或Web Worker的通信。 让我们看一下后者的一些示例代码。
工作人员的代码如下所示。 该代码尝试查找2到10 10之间的质数。 找到数字后,将报告结果。
(function (start, end) {
var n = start - 1;
while (n++ < end) {
var k = Math.sqrt(n);
var found = false;
for (var i = 2; !found && i <= k; ++i) {
found = n % i === 0;
}
if (!found) {
postMessage(n.toString());
}
}
})(2, 1e10);
传统上,Web worker(假定位于文件prime.js
)如下。 为简便起见,我们跳过了对Web Worker支持和返回结果合法性的检查。
var worker = new Worker('prime.js');
worker.addEventListener('message', function (ev) {
var primeNumber = ev.data * 1;
console.log(primeNumber);
}, false);
有关Web Worker和JavaScript多线程的更多详细信息,请参见文章Parallel JavaScript with Parallel.js 。
考虑上面的示例,我们知道素数在正整数之间遵循渐近分布。 对于x
到∞,我们获得x / log(x)
的分布。 这意味着我们将在开始时看到更多数字。 在这里,支票的价格也便宜得多(例如,我们开始时每单位时间收到的质数要多得多。)
这可以通过简单的时间轴和斑点来说明:
通过查看用户对搜索框的输入,可以给出一个不相关但相似的示例。 最初,用户可能会热衷于输入要搜索的内容; 但是,他的请求越具体,击键之间的时间差就越大。 提供显示实时结果的功能绝对是可取的,以帮助用户缩小请求范围。 但是,我们不希望对每个按键都执行请求,特别是因为第一个按键将非常快速地执行,并且无需考虑或不需要专门知识。
在这两种情况下,答案都是在给定的时间间隔内汇总以前的事件。 所描述的两种情况之间的区别在于,素数应始终在给定的时间间隔后显示(即,某些素数可能只是在表示上有所延迟)。 相反,如果在指定间隔内没有击键发生,搜索查询将仅触发新请求。 因此,一旦检测到按键,计时器将重置。
RxJS救援
Rx是一个库,用于使用可观察的集合来组成异步和基于事件的程序。 它以其声明性的语法和可组合性而著称,同时引入了简单的时间处理和错误模型。 考虑到我们以前的例子,我们对时间处理特别感兴趣。 尽管如此,我们将看到RxJS还有很多可以受益的地方。
RxJS的基本构建块是可观察者(生产者)和观察者(消费者)。 我们已经提到了两种可观察的类型:
- 即使我们未订阅热的可观察对象(例如,UI事件),它们也在推动。
- 冷观测只能在我们订阅时开始推动。 如果我们再次订阅,它们会重新开始。
冷可观察对象通常是指已转换为在RxJS中使用的数组或单个值。 例如,以下代码创建一个冷的可观察对象,该对象在完成之前仅产生一个值:
var observable = Rx.Observable.create(function (observer) {
observer.onNext(42);
observer.onCompleted();
});
我们还可以从可观察的创建函数返回一个包含清理逻辑的函数。
订阅可观察者独立于可观察者的种类。 对于这两种类型,我们都可以提供三个函数,它们满足通知语法的基本要求,包括onNext
, onError
和onCompleted
。 onNext
回调是必需的。
var subscription = observable.subscribe(
function (value) {
console.log('Next: %s.', value);
},
function (ev) {
console.log('Error: %s!', ev);
},
function () {
console.log('Completed!');
}
);
subscription.dispose();
作为最佳实践,我们应该使用dispose
方法终止订阅。 这将执行所有必需的清理步骤。 否则,有可能防止垃圾回收清理未使用的资源。
如果没有subscribe
,则变量observable中包含的observable
只是一个冷的observable。 但是,也可以使用publish
方法将其转换为热序列(即,我们执行伪订阅)。
var hotObservable = observable.publish();
RxJS中包含的一些帮助器仅处理现有数据结构的转换。 在JavaScript中,我们可以区分以下三个:
- 承诺返回单个异步结果,
- 单个结果的功能 ,以及
- 用于提供迭代器的生成器。
后者是ES6的新功能,对于ES5或更旧的版本,后者可以用数组替换(即使这是一个不好的替代品,应将其视为单个值)。
RxJS现在引入了一种数据类型,用于提供异步的多个(返回)值支持。 因此,现在已填写四个象限。
当需要拉迭代器时,将观察值的值推入。 一个示例是事件流,在该事件流中,我们无法强制发生下一个事件。 我们只能等待事件循环通知。
var array = [1,2,3,4,5];
var source = Rx.Observable.from(array);
创建或处理可观察对象的大多数助手还接受调度程序,该调度程序控制何时开始订阅以及何时发布通知。 我们将在这里不做详细介绍,因为默认调度程序在大多数实际情况下都可以正常工作。
RxJS中的许多运算符都引入了并发性,例如throttle
, interval
或delay
。 现在,我们将再看看前面的示例,这些示例在这些示例中变得至关重要。
例子
首先,让我们看一下质数发生器。 我们希望在给定时间内汇总结果,以使UI(尤其是开始时)不必处理太多更新。
在这里,我们实际上可能想结合使用RxJS的buffer
函数和前面提到的interval
帮助器。
结果应由下图表示。 绿色斑点在指定的时间间隔(由用于构造interval
的时间给定)之后出现。 缓冲区将在此间隔内聚集所有可见的蓝色斑点。
此外,我们还可以引入map
,这有助于我们转换数据。 例如,我们可能要转换接收到的事件参数,以获取传输的数据为数字。
var worker = new Worker('prime.js');
var observable = Rx.Observable.fromEvent(worker, 'message')
.map(function (ev) { return ev.data * 1; })
.buffer(Rx.Observable.interval(500))
.where(function (x) { return x.length > 0; })
.map(function (x) { return x.length; });
fromEvent
函数使用标准事件发射器模式从任何对象构造可观察对象。 buffer
还将返回长度为零的数组,这就是为什么我们引入where
函数将流减少为非空数组的原因。 最后,在此示例中,我们仅对生成的质数的数量感兴趣。 因此,我们映射缓冲区以获得其长度。
另一个示例是搜索查询框,应对其进行限制,使其仅在特定的空闲时间后才启动请求。 在这种情况下,有两个功能可能有用: throttle
功能产生在指定时间窗口内看到的第一个条目。 debounce
功能产生在指定时间窗口内看到的最后一个条目。 时间窗口也相应地移动(即相对于第一项/最后一项)。
我们想要实现下图所反映的行为。 因此,我们将使用debounce
机制。
我们要舍弃所有先前的结果,而仅获得时间窗口耗尽之前的最后一个结果。 假设输入字段具有id query
我们可以使用以下代码:
var q = document.querySelector('#query');
var observable = Rx.Observable.fromEvent(q, 'keyup')
.debounce(300)
.map(function (ev) { return ev.target.value; })
.where(function (text) { return text.length >= 3; })
.distinctUntilChanged()
.map(searchFor)
.switch()
.where(function (obj) { return obj !== undefined; });
在此代码中,窗口设置为300ms。 另外,我们限制查询至少3个字符的值,这与以前的查询不同。 这消除了通过键入某些内容并擦除它来纠正刚输入的不必要请求。
在整个表达式中有两个关键部分。 一种是使用searchFor
将查询文本转换为请求,另一种是switch()函数。 后者采用任何返回嵌套可观察值并仅从最新可观察序列产生值的函数。
创建请求的功能可以定义如下:
function searchFor(text) {
var xhr = new XMLHttpRequest();
xhr.open('GET', apibaseUrl + '?q=' + text, true);
xhr.send();
return Rx.Observable.fromEvent(xhr, 'load').map(function (ev) {
var request = ev.currentTarget;
if (request.status === 200) {
var response = request.responseText;
return JSON.parse(response);
}
});
}
注意嵌套的observable(可能导致无效请求undefined
),这就是我们链接switch()
和where()
。
结论
RxJS使JavaScript中的反应式编程成为一个快乐的现实。 作为替代,还有Bacon.js ,其工作原理类似。 尽管如此,RxJS最好的事情之一就是Rx本身,它可以在许多平台上使用。 这使得向其他语言,平台或系统的过渡非常容易。 它还将简洁和可组合的一组方法统一了反应式编程的一些概念。 此外,存在一些非常有用的扩展,例如RxJS-DOM ,它简化了与DOM的交互。
您在哪里看到RxJS大放异彩?
From: https://www.sitepoint.com/functional-reactive-programming-rxjs/