响应式
现实世界相当混乱:事件不按照顺序发生,应用崩溃,网络不通。几乎没有应用是完全同步的,所以我们不得不写一些异步代码保持应用的可响应性。大多数的时候是很痛苦的,但也并不是不可避免。
现代应用需要超级快速的响应速度,并且希望能够不漏掉一个字节的处理来自不同数据源的数据。然而并没有现成的解决方案,因为它们不会随着我们添加并发和应用程序状态而扩展代码变得越来越复杂。
本章向您介绍反应式编程,这是一种自然,简单的方法处理异步代码的方式。我会告诉你事件的流程 - 我们称之为Observables - 是处理异步代码的一种很好的方式。 然后我们将创建一个Observable,看看响应式思维和RxJS是怎么样改善现有技术,让你成为更快乐,更多高效的程序员。
什么是响应式?
让我们从一个小的响应性RxJS程序开始。 这个程序需要通过单击按钮检索来自不同来源的数据,它具有以下要求:
- 它必须统一来自使用不同源的JSON结构
- 最终结果不应包含任何副本
- 为了避免多次请求数据,用户不能重复点击按钮
使用RxJS,我们的代码类似这样:
代码语言:javascript复制var button = document.getElementById('retrieveDataBtn');
var source1 = Rx.DOM.getJSON('/resource1').pluck('name');
var source2 = Rx.DOM.getJSON('/resource2').pluck('props', 'name');
function getResults(amount) {
return source1.merge(source2)
.pluck('names')
.flatMap(function(array) { return Rx.Observable.from(array); })
.distinct()
.take(amount);
}
var clicks = Rx.Observable.fromEvent(button, 'click');
clicks.debounce(1000)
.flatMap(getResults(5))
.subscribe(
function(value) { console.log('Received value', value); },
function(err) { console.error(err); },
function() { console.log('All values retrieved!'); }
);
不要担心不理解这里的代码。只要关注于成果即可。你看到的第一件事是我们使用更少的代码实现更多的功能。我们通过使用Observable来实现这一目标。
Observable表示数据流。程序也可以可以主要表示为数据流。在前面的示例中,两个远程源是Observables,用户点击鼠标也是如此。实际上,我们的程序本质上是一个由按钮的单击事件构成的Observable,我们把它转变成获得我们想要的结果。
响应式编程具有很强的表现力,举个例子来说,限制鼠标重复点击的例子。想象一下我们使用我们使用promise和callback实现这个功能是有多复杂:我们需要每秒重置一下点击次数,并且在用户点击之后每秒都要保存点击状态。但是这样子,对于这个小功能来说就显得过于复杂了,并且所写代码与业务功能并没有直观的联系。为了弥补基础代码库的功能不足,在一个大型应用中,这些很小的复杂功能会增加的非常快。
通过响应式编,我们使用debounce方法来限制点击流次数。这样就保证每次点击的间隔时间至少1s,忽略1s之间的点击次数。我们不关心内部如何实现,我们只是表达我们希望代码执行的操作,而不是如何操作。
这就变得更有趣了。接下来,您将看到反应式编程如何帮助我们提高课程效率和表现力。
电子表格是可响应的
让我们从这样一个响应性系统的典型例子开始考虑:点子表格。我们都是使用过吧,但我们很少停下来思考它们是多么令人震惊的直观。假设我们在电子表格的单元格A1中有一个值,然后我们可以在电子表格中的其他单元格中引用它,并且每当我们更改A1时,每个依赖于A1的单元格都会自动更新与A1同步。
这些操作对我们感觉很自然,我们并不会去告诉计算机去根据A1更新单元格或者如何更新;这些单元格就自动这样子做了。在点子表格中,我们只需要简单的声明我们需要处理的问题,不用操心计算机如何处理。
鼠标输入作为streams
理解如何把事件作为流,我们回想一下本章开头的那个程序。在那里,我们使用鼠标点击作为用户点击时实时生成的无限事件流。这个想法起源于Erik Meijer,也就是Rxjs的作者。他认为:你的鼠标就是一个数据库。
在响应式编程中,我把鼠标点击事件作为一个我们可以查询和操作的持续的流事件。想象成流而不是一个孤立的事件,这种想法开辟了一种全新的思考方式。我们可以在其中操纵尚未创建的整个值的流。
好好想想。这种方式有别于我们以往的编程方式,之前我们把数据存储在数据库,或者数组并且等待这些数据可用之后在使用它们。如果它们尚不可用(举个例子:一个网络请求),我们只能等它们好了才可以使用。
我们可以将流视为所在由时间而不是存储位置分开的数组。无论是时间还是存储位,我们都有元素序列:
将您的程序视为流动的数据序列是理解的RxJS程序的关键。这需要一些练习,但并不难。事实上,大多数我们在任何应用程序中使用的数据都可以表示为序列。
序列查询
让我们使用传统javascript传统的事件绑定技术来实现一个鼠标点击流。要记录鼠标点击的x和y坐标,我们可以这样写:
ch1/thinking_sequences1.js
代码语言:javascript复制document.body.addEventListener('mousemove', function(e) {
console.log(e.clientX, e.clientY);
});
此代码将按顺序打印每次鼠标单击的x坐标和y坐标。
输出如下:
代码语言:javascript复制252 183
211 232
153 323
...
看起来像一个序列,不是吗? 当然,问题在于操纵事件并不像操纵数组那么容易。 例如,如果我们想要更改前面的代码,使其仅记录前10次位于屏幕右侧的单击事件(相当随机的目标),我们会写像这样的东西:
代码语言:javascript复制var clicks = 0;
document.addEventListener('click', function registerClicks(e) {
if (clicks < 10) {
if (e.clientX > window.innerWidth / 2) {
console.log(e.clientX, e.clientY);
clicks = 1;
}
} else {
document.removeEventListener('click', registerClicks);
}
});
为了满足我们的要求,我们通过引入一个全局变量作为扩展状态来记录当前点击数。 我们还需要使用嵌套的条件来检查两个不同的条件。当我们完成时,我们必须注销事件,以免泄漏内存。
副作用和外部状态如果一个动作在其发生的范围之外产生影响,我们称之为一方副作用。更改函数外部的变量,打印到控制台或更新数据库中的值,这些都是副作用。例如改变函数内部的变量是安全的,但是如果该变量超出了我们函数的范围,那么其他函数也可以改变它的值,这就意味着这个功能不再受控制,因为你无法预测外部会对这个变量作何操作。所以我们需要跟踪它,添加检查以确保它的变化符合我们的预期。但是这样子添加的代码其实与我们程序无关,确增加程序的复杂度也更容易出错。虽然副作用总是会有的,但是我们应该努力减少。这在响应式编程中尤其重要,因为我们随着时间变换会产生很多状态片段。所以避免外部状态和副作用是贯穿本书一条宗旨。
我们设法满足了我们的简单要求,但是为了实现这样一个简单的目标,最终得到了相当复杂的代码。对于首次查看它的开发人员来说,不容易懂且维护代码很困难。 更重要的是,因为我们仍然需要保存外部撞他,所以我们很容易在未来发展出玄妙的错误。
在这种情况下我们想要的只是查询点击的“数据库”。如果我们是使用关系数据库,我们使用声明性语言SQL:
代码语言:javascript复制SELECT x, y FROM clicks LIMIT 10
如果我们将点击事件流视为可以查询和转变的数据源,该怎么办?毕竟,它与数据库没有什么不同,都是一个可以处理数据的东西。我们所需要的只是一个为我们提供抽象概念的数据类型。
输入RxJS及其Observable数据类型:
代码语言:javascript复制Rx.Observable.fromEvent(document, 'click')
.filter(function(c) { return c.clientX > window.innerWidth / 2; })
.take(10)
.subscribe(function(c) { console.log(c.clientX, c.clientY) })
这段代码功能同之前,它可以这样子解读:
创建一个Observable的点击事件,并过滤掉在点击事件上发生屏幕左侧的点击。然后只在控制台打印前10次点击的坐标。
注意即使您不熟悉代码也很容易阅读,也没有必要创建外部变量来保持状态。这样使我们的代码是自包含的,不容易产生bug。所以也就没必要去清除你的状态。我们可以合并,转换或者单纯的传递Observables。我们已经将不容易处理的事件转变为有形数据结构,这种数据结构与数组一样易于使用,但更加灵活。
在下一节,我们将看到使Observables如此强大的原理。
观察者和迭代者
要了解Observable的来源,我们需要查看他们的基础:Observer和Iterator软件模式。在本节中我们将快速浏览它们,然后我们将看到Observables如何结合,简单而有力。
观察者模式
对于软件开发人员来说,很难不听到Observables就想起观察者模式。在其中我们有一个名为Producer的对象,内部保留订阅者的列表。当Producer对象发生改变时,订阅者的update方法会被自动调用。(在观察者模式的大部分解释中,这个实体被叫做Subject,为了避免大家和RxJs的自己Subject混淆,我们称它为Producer)。
ch1/observer_pattern.js
代码语言:javascript复制function Producer() {
this.listeners = [];
}
Producer.prototype.add = function(listener) {
this.listeners.push(listener);
};
Producer.prototype.remove = function(listener) {
var index = this.listeners.indexOf(listener);
this.listeners.splice(index, 1);
};
Producer.prototype.notify = function(message) {
this.listeners.forEach(function(listener) {
listener.update(message);
});
};
Producer对象在实例的侦听器中保留一个动态的Listener列表,每当Producer更新的时候都会调用其notify方法。在下面的代码中,我们创建了两个对象来监听
notifie
,一个Producer
的实例。
ch1/observer_pattern.js
代码语言:javascript复制// Any object with an 'update' method would work.
var listener1 = {
update: function(message) {
console.log('Listener 1 received:', message);
}
};
var listener2 = {
update: function(message) {
console.log('Listener 2 received:', message);
}
};
var notifier = new Producer();
notifier.add(listener1);
notifier.add(listener2);
notifier.notify('Hello there!');
当我们运行这个程序的时候:
代码语言:javascript复制Listener 1 received: Hello there!
Listener 2 received: Hello there!
当notifier
更新内部状态的时候,listener1
和listener2
都会被更新。这些都不需要我们去操心。
我们的实现很简单,但它说明了观察者模式允许观察者和监听器解耦。
迭代器模式
Observable的另一主要部分来自Iterator模式。一个Iterator是一个为消费者提供简单的遍象它内容的方式,隐藏了消费者内部的实现。
Iterator接口很简单。它只需要两个方法:next()来获取序列中的下一个项目,以及hasNext()来检查是否还有项目序列。
下面是我们如何编写一个对数字数组进行操作的迭代器,并且只返回divisor
参数的倍数的元素:
ch1/iterator.js
代码语言:javascript复制function iterateOnMultiples(arr, divisor) {
this.cursor = 0;
this.array = arr;
this.divisor = divisor || 1;
}
iterateOnMultiples.prototype.next = function() {
while (this.cursor < this.array.length) {
var value = this.array[this.cursor ];
if (value % this.divisor === 0) {
return value;
}
}
};
iterateOnMultiples.prototype.hasNext = function() {
var cur = this.cursor;
while (cur < this.array.length) {
if (this.array[cur ] % this.divisor === 0) {
return true;
}
}
return false;
};
我们可以这样子使用我们的迭代器:
ch1/iterator.js
代码语言:javascript复制var consumer = new iterateOnMultiples([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3);
console.log(consumer.next(), consumer.hasNext()); // 3 true
console.log(consumer.next(), consumer.hasNext()); // 6 true
console.log(consumer.next(), consumer.hasNext()); // 9 false
迭代器非常适合封装任何类型数据结构的遍历逻辑。 正如我们在前面的例子中看到的那样,迭代器在处理不同类型的数据的时候就会变得很有趣,或者在运行的时候做配置,就像我们在带有divisor参数的示例中所做的那样。
Rx模式和Observable
虽然Observer和Iterator模式本身就很强大,但是两者的结合甚至更好。 我们称之为Rx模式,命名为 在Reactive Extensions库之后。我们将在本书的其余部分使用这种模式。
Observable序列或简单的Observable是Rx模式的核心。Observable按顺序传递出来它的值 - 就像迭代器一样 - 而不是消费者要求它传出来的值。这个和观察者模式有相同之处:得到数据并将它们推送到监听器。
pull和push在编程中,基于推送的行为意味着应用程序的服务器组件向其客户端发送更新,而不是客户端必须轮询服务器以获取这些更新。这就像是说“不要打电话给我们; 我们会打电话给你。“ RxJS是基于推送的,因此事件源(Observable)将推动新值给消费者(观察者),消费者却不能去主动请求新值。
更简单地说,Observable是一个随着时间的推移可以使用其数据的序列。Observables,也就是Observers的消费者相当于观察者模式中的监听器。当Observe订阅一个Observable时,它将在序列中接收到它们可用的值,而不必主动请求它们。
到目前为止,似乎与传统观察者没有太大区别。 但实际上有两个本质区别:
- Observable在至少有一个Observer订阅它之前不会启动。
- 与迭代器一样,Observable可以在序列完成时发出信号。
使用Observables,我们可以声明如何对它们发出的元素序列做出反应,而不是对单个项目做出反应。我们可以有效地复制,转换和查询序列,这些操作将应用于序列的所有元素。
创建Observables
有几种方法可以创建Observable,创建函数是最明显的一种。 Rx.Observable对象中的create方法接受一个Observer参数的回调。 该函数定义了Observable将如何传出值。这是我们如何创建一个简单的Observable:
代码语言:javascript复制var observable = Rx.Observable.create(function(observer) {
observer.onNext('Simon');
observer.onNext('Jen');
observer.onNext('Sergi');
observer.onCompleted(); // We are done
});
当我们订阅此Observable时,它通过在其侦听器上调用onNext方法来发出三个字符串。 然后它调用onCompleted来表示序列已完成。 但是我们究竟如何订阅Observable呢?我们使用Observers来做这件事情。
第一次接触Observers
Observers监听Observables。每当Observable中触发一个事件,它都会在所有Observers中调用相关的方法。
Observers有三种方法:onNext
,onCompleted
和onError
:
onNext 相当于观察者模式中的update
。 当Observable发出新值时调用它。请注意该名称如何反映我们订阅序列的事实,而不仅仅是离散值。
onCompleted 表示没有更多可用数据。 调用onCompleted后,对onNext的进一步调用将不起作用。
onError 在Observable中发生错误时调用。 在调用之后,对onNext的进一步调用将不起作用
以下是我们创建基本观察者的方法:
代码语言:javascript复制var observer = Rx.Observer.create(
function onNext(x) { console.log('Next: ' x); },
function onError(err) { console.log('Error: ' err); },
function onCompleted() { console.log('Completed'); }
);
Rx.Observer对象中的create方法接受onNext,onCompleted和onError情况的函数,并返回一个Observer实例。这三个函数是可选的,您可以决定要包含哪些函数。例如,如果我们订阅无限序列(例如点击按钮(用户可以永久点击)),则永远不会调用onCompleted处理程序。 如果我们确信序列不能出错(例如,通过从数组中生成一个Observable),我们就不需要onError方法了。
使用Observable进行Ajax调用
我们还没有对Observables做过任何实用的事情。如何创建一个检索远程内容的Observable?为此,我们将使用Rx.Observable.create包装XMLHttpRequest对象。
代码语言:javascript复制function get(url) {
return rxjs.Observable.create(function(observer) {
// Make a traditional Ajax request
var req = new XMLHttpRequest(); req.open('GET', url);
req.onload = function() {
if (req.status == 200) {
// If the status is 200, meaning there have been no problems,
// Yield the result to listeners and complete the sequence
observer.next(req.response);
observer.completed();
}
else {
// Otherwise, signal to listeners that there has been an error
observer.error(new Error(req.statusText)); }
};
req.onerror = function() {
observer.error(new Error("Unknown Error"));
};
req.send();
});
}
// Create an Ajax Observable
var test = get('/api/contents.json');
在前面的代码中,get函数使用create来包装XMLHttpRequest。如果HTTP GET请求成功,我们emit它的内容并结束序列(我们的Observable只会发出一个结果)。 否则,我们会emit一个错误。在最后一行,我们传入一个url进行调用。 这将创建Observable,但它不会发出任何请求。这很重要:Observable在至少有一个观察者描述它们之前不会做任何事情。 所以让我们要这样子做:
代码语言:javascript复制// Subscribe an Observer to it
test.subscribe(
function onNext(x) { console.log('Result: ' x); },
function onError(err) { console.log('Error: ' err); },
function onCompleted() { console.log('Completed'); }
);
首先要注意的是,我们没有像之前的代码那样显式创建Observer。大多数时候我们都会使用这个更短的版本,我们在Observable中使用这三个订阅Observer案例的函数:next,completed和error。
subscribe
然后一切就绪。在subscribe
之前,我们只是声明了Observable和Observer将如何交互。只有当我们调用subscribe
方法时,一切才开始运行。
始终会有一个Operator
在RxJS中,转换或查询序列的方法称为Operator。Operator位于静态Rx.Observable
对象和Observable
实例中。在我们的示例中,create
就是一个这样的Operator。
当我们必须创建一个非常具体的Observable时,create
是一个很好的选择,但是RxJS提供了许多其他Operator,可以很容易地为常用源创建Observable。
让我们再看看前面的例子。对于像Ajax请求这样的常见操作,通常有一个Operator可供我们使用。 在这种情况下,RxJS DOM库提供了几种从DOM相关源创建Observable的方法。由于我们正在执行GET请求,我们可以使用Rx.DOM.Request.get,然后我们的代码就变成了这个:
代码语言:javascript复制Rx.DOM.get('/api/contents.json').subscribe(
function onNext(data) { console.log(data.response); },
function onError(err) { console.error(err); }
);
rxjs-dom本身支持的rxjs版本比较旧,例子只能做为示意
这段代码与我们之前的代码完全相同,但我们不必创建XMLHttpRequest的包装器: 它已经存在了。另请注意,这次我们省略了onCompleted回调,因为我们不打算在Observable complete时做出反应。我们知道它只会产生一个结果,我们已经在onNext回调中使用它了。
在本书中我们将使用这样的大量便利操作符。这都是基于rxjs本身的能量,这也正式rxjs强大的地方之一。
一种可以约束全部的数据类型在RxJS程序中,我们应该努力将所有数据都放在Observables中,而不仅仅是来自异步源的数据。 这样做可以很容易地组合来自不同来源的数据,例如现有数组与回调结果,或者XMLHttpRequest的结果与用户触发的某些事件。 例如,如果我们有一个数组,其项目需要与来自其他地方的数据结合使用,最好将此数组转换为Observable。(显然,如果数组只是一个不需要组合的中间变量,则没有必要这样做。)在本书中,您将了解在哪些情况下值得将数据类型转换为Observables。
RxJS为operators提供了从大多数JavaScript数据类型创建Observable的功能。 让我们回顾一下你将一直使用的最常见的:数组,事件和回调。
从数组创建Observable
我们可以使用通用的operators将任何类似数组或可迭代的对象转换为Observable。 from
将数组作为参数并返回一个包含他所有元素的Observable。
Rx.Observable
.from(['Adrià', 'Jen', 'Sergi'])
.subscribe(
function(x) { console.log('Next: ' x); },
function(err) { console.log('Error:', err); },
function() { console.log('Completed'); }
);
from
是和fromEvent
一起,是RxJS代码中最方便和最常用的operators之一。
从JavaScript事件创建Observable
当我们将一个事件转换为一个Observable时,它就变成了一个可以组合和传递的第一类值。 例如,这是一个Observable,只要它移动就会传初鼠标指针的坐标。
代码语言:javascript复制var allMoves = Rx.Observable.fromEvent(document, 'mousemove')
allMoves.subscribe(function(e) {
console.log(e.clientX, e.clientY);
});
将事件转换为Observable会将事件从之前的事件逻辑中释放出来。更重要的是,我们可以基于原始的Observables创建新的Observable。这些新的是独立的,可用于不同的任务。
代码语言:javascript复制var movesOnTheRight = allMoves.filter(function(e) {
return e.clientX > window.innerWidth / 2;
});
var movesOnTheLeft = allMoves.filter(function(e) {
return e.clientX < window.innerWidth / 2;
});
movesOnTheRight.subscribe(function(e) {
console.log('Mouse is on the right:', e.clientX);
});
movesOnTheLeft.subscribe(function(e) {
console.log('Mouse is on the left:', e.clientX);
});
在前面的代码中,我们从原始的allMoves中创建了两个Observable。 这些专门的Observable只包含原始的过滤项:movesOnTheRight包含发生在屏幕右侧的鼠标事件,movesOnTheLeft包含发生在左侧的鼠标事件。 它们都没有修改原始的Observable:allMoves将继续发出所有鼠标移动。 Observable是不可变的,每个应用于它们的operator都会创建一个新的Observable。
从回调函数创建Observable
如果您使用第三方JavaScript库,则可能需要与基于回调的代码进行交互。 我们可以使用fromCallback
和fromNodeCallback
两个函数将回调转换为Observable。Node.js遵循的是在回调函数的第一个参数传入错误对象,表明存在问题。然后我们使用fromNodeCallback
专门从Node.js样式的回调中创建Observable:
var Rx = require('rx'); // Load RxJS
var fs = require('fs'); // Load Node.js Filesystem module
// Create an Observable from the readdir method
var readdir = Rx.Observable.fromNodeCallback(fs.readdir); // Send a delayed message
var source = readdir('/Users/sergi');
var subscription = source.subscribe(
function(res) { console.log('List of directories: ' res);},
function(err) { console.log('Error: ' err); },
function() { console.log('Done!');
});
前面的代码中,我们使用Node.js的fs.readdir方法创建一个Observable readdir。 fs.readdir接受目录路径和回调函数delayedMsg,该函数在检索目录内容后调用。
我们使用readdir和我们传递给原始fs.readdir的相同参数,省掉了回调函数。 这将返回一个Observable,当我们订阅一个Observer时,它将正确使用onNext,onError和onCompleted。
总结
在本章中,我们探讨了响应式编程,并了解了RxJS如何通过Observable解决其他问题的方法,例如callback或promise。现在您了解为什么Observables功能强大,并且您知道如何创建它们。有了这个基础,我们现在可以继续创建更有趣的响应式程序。下一章将向您展示如何创建和组合基于序列的程序,这些程序为Web开发中的一些常见场景提供了更“可观察”的方法。