rxjs react
Dealing with asynchronous non-blocking processing has always been the norm in the JavaScript world, and now is becoming very popular in many other contexts. The benefits are clear: an efficient use of resources. But the benefits come at a cost: a non-trivial increase in complexity.
异步非阻塞处理一直是JavaScript世界中的常态,现在在许多其他情况下变得非常流行。 好处很明显:有效利用资源。 但是,好处是要付出代价的:复杂性非同寻常地增加。
Over time, vendors and the open source community have tried to find ways to reduce such complexity without compromising the benefits.
随着时间的流逝,供应商和开放源代码社区已尝试找到在不损害收益的情况下降低此类复杂性的方法。
Asynchronous processing started with ‘callbacks’, then came Promise and Future, async and await. Recently another kid has come to town — ReactiveX with its various language implementations — bringing the developers a new powerful tool, the Observable.
异步处理从“回调”开始,然后是Promise and Future,异步并等待。 最近,另一个孩子来到了这里-具有各种语言实现的ReactiveX-为开发人员带来了一个新的强大工具Observable。
In this article, we want to show how Observables implemented by RxJs (the JavaScript embodiment of ReactiveX) can simplify code to be executed with Node.js, the popular server-side JavaScript non-blocking environment.
在本文中,我们想展示由RxJs实现的Observables (ReactiveXJavaScript实施例)如何简化通过Node.js(流行的服务器端JavaScript非阻塞环境)执行的代码。
一个简单的用例—读取,转换,写入和记录 (A simple use case — Read, Transform, Write, and Log)
To make our reasoning concrete, let’s start from a simple use case. Let’s assume we need to read the files contained in Source Dir
, transform their content and write the new transformed files in a Target Dir
, while keeping a log of the files we have created.
为了使我们的推理更具体,让我们从一个简单的用例开始。 假设我们需要阅读Source Dir
包含的文件, 转换其内容并将新的转换文件写入Target Dir
,同时保留我们创建的文件的日志。
同步执行 (Synchronous implementation)
The synchronous implementation of this use case is pretty straightforward. In a sort of pseudo code representation, we could think of something like:
此用例的同步实现非常简单。 在某种伪代码表示中,我们可以想到以下内容:
read the names of the files of Source Dir
for each file name
read the file
transform the content
write the new file in Target Dir
log the name of the new file
end for
console.log('I am done')
There is nothing special to comment here. We can just say that we are sure of the sequence of execution of each line and that we are sure that things will happen as described by the following flow of events. Each circle corresponds to the completion of an I/O operation.
这里没有什么特别的评论。 我们可以肯定地说,我们确定每一行的执行顺序,并且可以确定事件将按照以下事件流的描述进行。 每个圆圈对应一个I / O操作的完成。
在异步非阻塞环境(如Node.js)中会发生什么 (What happens in an asynchronous non-blocking environment like Node.js)
Node.js is an asynchronous non-blocking execution environment for JavaScript. Non-blocking means that Node.js does not wait for I/O or Network operations to complete before moving to the execution of the next line of code.
Node.js是JavaScript的异步非阻塞执行环境。 非阻塞意味着Node.js在进入下一行代码执行之前不会等待I / O或网络操作完成。
处理一个文件 (Processing one file)
Reading and writing files are I/O operations where Node.js shows its non-blocking nature. If a Node.js program asks for a file to read, it has to provide a function to be executed when the file content is available (the so called callback) and then immediately move on to the next operation to execute.
读写文件是I / O操作,其中Node.js表现出其非阻塞性。 如果Node.js程序要求读取文件,则它必须提供一个在文件内容可用时要执行的功能(所谓的callback ),然后立即继续执行下一个操作。
Let’s consider the case of just one file. Reading, transforming, writing one file and updating the log in Node.js looks something like this:
让我们考虑一个文件的情况。 在Node.js中读取,转换,写入一个文件并更新日志看起来像这样:
import * as fs from 'fs'; // Node module to access file system
const fileName = 'one-file.txt';
fs.readFile(fileName, callback(err, data) => {
const newContent = transform(data);
const newFileName = newFileName(fileName); // calculate new name
fs.writeFile(newFileName, newContent, err => {
if(err) {// handle error};
fs.appendFile('log.txt', newFileName + ' written', err = {
if (err) {// handle error}
});
});
})
The syntax may look a bit convoluted with 2 levels of indentation, but if we think of what happens in terms of events we can still precisely foresee the sequence:
语法在2个缩进级别上看起来有些复杂,但是如果我们考虑事件发生了什么,我们仍然可以精确地预测序列:
无极的天堂 (The paradise of Promise)
This is the use case where JavaScript Promise shines. Using Promise we can make the code look again sequential, without interfering with the asynchronous nature of Node.js.
这是JavaScript Promise发挥作用的用例。 使用Promise,我们可以使代码再次按顺序显示,而不会干扰Node.js的异步特性。
Assuming we can access functions that perform read and write operations on file and return a Promise, then our code would look like:
假设我们可以访问对文件执行读写操作并返回Promise的函数,则我们的代码如下所示:
const fileName = 'my-file.txt';
readFilePromise(fileName)
.then(data => {
const newContent = transform(data);
const newFileName = newFileName(fileName); // build the new name
return writeFilePromise(newFileName, newContent)
})
.then(newFileName => appendFile('log.txt', newFileName))
.then(newFileName => console.log(newFileName + ' written'))
.catch(err => // handle error)
There are several ways to transform Node.js functions in Promise
based functions. This is one example:
有多种方法可以在基于Promise
的函数中转换Node.js函数。 这是一个例子:
function readFilePromise(fileName: string): Promise<Buffer>{
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function(err, data: Buffer) {
if(err !== null) return reject(err);
resolve(data);
});
});
}
处理许多文件 (Processing many files)
If we return to the original use case, where we have to transform all the files contained in a Directory, the complexity increases and Promises start showing some limits.
如果返回到原始用例,即必须转换目录中包含的所有文件,那么复杂性就会增加,并且Promises开始显示一些限制。
Let’s look at the events the Node.js implementation needs to manage:
让我们看一下Node.js实现需要管理的事件:
Each circle represents the completion of one I/O operation, either read or write. Every line represents the processing of one specific file, or a chain of Promises.
每个圆圈代表一个I / O操作(读或写)的完成。 每行代表一个特定文件或一系列承诺的处理。
Given the non-blocking nature of Node.js, there is no certainty on the sequence in time of such events. It is possible that we will finish writing File2
before we finish reading File3
.
鉴于Node.js的非阻塞性质,此类事件的时间顺序不确定。 我们可能会完成File2
编写 在完成阅读File3
之前。
The parallel processing of each file makes the use of Promises more complex (at the end of this article, a Promise based implementation is provided). This is the scenario where ReactiveX — RxJs in particular — and Observable shine and allow you to build elegant solutions.
每个文件的并行处理使Promises的使用更加复杂(在本文结尾处,提供了一个基于Promise的实现)。 在这种情况下,ReactiveX(尤其是RxJ)和可观察到的光芒使您可以构建优雅的解决方案。
什么是可观察物,您可以如何使用它们? (What are Observables and what can you do with them?)
There are many places where formal definitions of Observables are detailed, starting from the official site of ReactiveX.
从ReactiveX的官方网站开始,有很多地方详细介绍了Observables的正式定义。
Here I just want to remind you of a couple of properties that have always gotten my attention:
在这里,我只想提醒您一些经常引起我注意的属性:
Observable models a stream of events
可观察模型的事件流
Observable is the “push” brother of Iterable, which is “pull”
可观察到的是Iterable的“推”兄弟,即“拉”
As the “push” brother of Iterable, Observable offers developers many of the cool features provided by Iterables such as:
作为Iterable的“推动者”兄弟,Observable为开发人员提供了Iterables提供的许多很酷的功能,例如:
Transform “streams of events” or Observables, via operators such as
map
,filter
andskip
通过诸如
map
,filter
和skip
运算符转换“事件流”或Observable- Apply functional programming style 应用函数式编程风格
One additional very important thing that Observable offers is subscription. Via subscription, the code can apply “side effects” to events and perform specific actions when specific events happen, such as when errors occur or the stream of events completes.
可观察提供的另一项非常重要的事情是订阅。 通过订阅,该代码可以将“副作用”应用于事件,并在特定事件发生时(例如发生错误或事件流完成时)执行特定操作。
As you can see, the Observable interface gives developers the possibility to provide three different functions which define what to do respectively when: an event is emitted with its data, an error occurs, or the stream of events completes.
如您所见,Observable接口使开发人员可以提供三种不同的功能,这些功能分别定义何时执行以下操作:随数据一起发出事件,发生错误或事件流完成。
I guess all of the above may sound very theoretical to those who have not yet played with Observable, but hopefully the next part of the discussion, which is focused on our use case, will make these concepts more concrete.
我想以上所有内容对于尚未使用Observable的用户来说可能听起来非常理论化,但是希望下一部分讨论(专注于我们的用例)将使这些概念更加具体。
通过Observable实现读取,转换,写入和日志用例 (Implementation of the Read, Transform, Write, and Log use case via Observable)
Our use case starts with reading the list of files contained in Source Dir
. So, let’s start from there.
我们的用例从读取Source Dir
包含的文件列表开始。 因此,让我们从那里开始。
读取目录中包含的所有文件名 (Read all the file names contained in a Directory)
Let’s assume we have access to a function which receives as input the name of a directory and returns an Observable which emits the list of file names of the directory once the directory tree structure has been read.
假设我们可以访问一个函数,该函数接收输入的目录名并返回一个Observable,一旦读取目录树结构,该Observable就会发出目录的文件名列表。
readDirObservable(dirName: string) : Observable<Array<string>>
We can subscribe to this Observable and when all file names have been read, start doing something with them:
我们可以订阅此Observable,并在读取所有文件名后,开始使用它们进行操作:
阅读文件列表 (Read a list of files)
Let’s assume now that we can access a function which receives as input a list of file names and emits each time a file has been read (it emits the content of the file Buffer
, and its name string
).
现在假设我们可以访问一个函数,该函数接收输入的文件名列表,并在每次读取文件时发出该消息(发出文件Buffer
的内容及其名称string
)。
readFilesObservable(fileList: Array<string>)
: Observable<{content: Buffer, fileName: string}>
We can subscribe to such Observable
and start doing something with the content of the files.
我们可以订阅这样的Observable
然后开始处理文件的内容。
合并Observables — switchMap运算符 (Combining Observables — switchMap operator)
We have now two Observables, one that emits a list of file names when the directory has been read and one that emits every time a file is read.
现在,我们有两个Observable,一个在读取目录时发出文件名列表,另一个在每次读取文件时发出文件名列表。
We need to combine them to implement the first step of our use case, which is: when readDirObservable
emits, we need to switch to readFilesObservable
.
我们需要将它们结合起来以实现用例的第一步,即:当readDirObservable
发出时,我们需要切换到readFilesObservable
。
The trick here is performed by the switchMap
operator. The code looks like:
这里的技巧是由switchMap
运算符执行的。 代码如下:
readDirObservable(dirName)
.switchMap(fileList => readFilesObservable(fileList))
.subscribe(
data => console.log(data.fileName + ‘ read’), // do stuff with the data received
err => { // manage error },
() => console.log(‘All files read’)
)
We must mention that the switchMap
operator is more powerful than this. Its full power though can not be appreciated in this simple use case, and its full description is outside the scope of this post. If you are interested, this is an excellent article that describes in detail switchMap
.
我们必须提到switchMap
运算符比这更强大。 尽管在这种简单的用例中无法理解它的全部功能,但其完整描述超出了本文的范围。 如果您有兴趣,这是一篇很棒的文章 ,详细介绍了switchMap
。
可观察的生成可观察的流 (Observable generating a stream of Observables)
We have now a stream of events representing the completion of a read
operation. After the read
we need to do a transformation of the content that, for sake of simplicity, we assume to be synchronous, and then we need to save the transformed content in a new file.
现在,我们有一系列事件表示read
操作的完成。 read
后,我们需要对内容进行转换,为简单起见,我们假定它是同步的,然后需要将转换后的内容保存在新文件中。
But writing a new file is again an I/O operation, or a non-blocking operation. So every ‘file-read-completion’ event starts a new path of elaboration that receives as input the content and the name of the source file, and emits when the new file is written in the Target Dir
(the event emitted carries the name of the file written).
但是,写入新文件还是一次I / O操作或非阻塞操作。 因此,每个“文件读取完成”事件都会启动一条新的详细说明路径,该路径将输入内容和源文件的名称作为输入,并在将新文件写入Target Dir
发出(发出的事件带有名称写入的文件)。
Again, we assume that we’re able to access a function that emits as soon as the write operation is completed, and the data emitted is the name of the file written.
再次,我们假设我们能够访问在写操作完成后立即发出的函数,并且发出的数据是写入文件的名称。
writeFileObservable(fileName: string, content: Buffer) : Observable<string>
In this case, we have different “write-file” Observables, returned by the writeFileObservable
function, which emits independently. It would be nice to merge them into a new Observable which emits any time each of these “write-file” Observables emit.
在这种情况下,我们有由writeFileObservable
函数返回的不同的“写入文件” Observable,它们独立发出。 最好将它们合并到一个新的Observable中,该Observable会在每个“写入文件” Observable发出时随时发出。
With ReactiveX (or RxJs in JavaScript) we can reach this result using the mergeMap
operator (also known as a flatMap). This is what the code looks like:
使用ReactiveX(或JavaScript中的RxJs),我们可以使用mergeMap
运算符(也称为flatMap )达到此结果。 代码如下所示:
readDirObservable(dir)
.switchMap(fileList => readFilesObservable(fileList))
.map(data => transform(data.fileName, data.content))
.mergeMap(data => writeFileObservable(data.fileName, data.content))
.subscribe(
file => console.log(data.fileName + ‘ written’),
err => { // manage error },
() => console.log(‘All files written’)
)
The mergeMap
operator has created a new Observable, the writeFileObservable
as illustrated in the following diagram:
mergeMap
运算符创建了一个新的Observable,即writeFileObservable
,如下图所示:
所以呢? (So what?)
Applying the same approach, if we just imagine that we have a new function of writeLogObservable
, that writes a line on the log as soon as the file is written and emits the file name as soon as the log is updated, the final code for our use case would look like:
应用相同的方法,如果我们只是想像一下我们有一个新的writeLogObservable
函数,该函数会在写入文件后立即在日志上写一行,并在更新日志后立即发出文件名,这是我们的最终代码用例如下所示:
readDirObservable(dir)
.switchMap(fileList => readFilesObservable(fileList))
.map(data => transform(data.fileName, data.content))
.mergeMap(data => writeFileObservable(data.fileName, data.content))
.mergeMap(fileName => writeLogObservable(fileName))
.subscribe(
file => console.log(fileName + ‘ logged’),
err => { // manage error },
() => console.log(‘All files have been transformed’)
)
We do not have indentations introduced by the callbacks.
回调没有引入缩进。
Time flows along the vertical axis only, so we can read the code line by line and reason about what is happening line after line.
时间仅沿着垂直轴流动,因此我们可以逐行读取代码并逐行读取发生的原因。
We have adopted a functional style.
我们采用了一种功能风格。
In other words, we have seen the benefits of Observable in action.
换句话说,我们已经在行动中看到了可观察的好处。
使用回调从函数创建Observable (Create Observable from functions with callbacks)
I hope you now think that this looks pretty cool. But even in this case you may have one question. All the functions that make this code cool just do not exist. There is no readFilesObservable
or writeFileObservable
in standard Node.js libraries. How can we create them?
我希望您现在认为这看起来很酷。 但是即使在这种情况下,您可能也会有一个问题。 所有使该代码变酷的功能都不存在。 标准Node.js库中没有readFilesObservable
或writeFileObservable
。 我们如何创建它们?
bindCallback和bindNodeCallback (bindCallback and bindNodeCallback)
A couple of functions provided by Observable, namely bindCallback
(and bindNodeCallback
) come to our rescue.
Observable提供的几个功能,即bindCallback
(和bindNodeCallback
)可以帮助我们。
The core idea behind them is to provide a mechanism to transform a function f
which accepts a callback cB(cBInput)
as input parameter into a function which returns an Observable obsBound
which emits cBInput
. In other words, it transforms the invocation of the cB
in the emission of cBInput
.
它们背后的核心思想是提供一种机制,该机制将接受回调cB(cBInput)
作为输入参数的函数f
转换为返回发出cBInput
的Observable obsBound
cBInput
。 换句话说,它转换了cB
的调用 在cBInput
的发射中。
The subscriber of obsBound
can define the function which will process cBInput
(which plays the same role as cB(cBInput)
). The convention applied is that the callback function cB(cBInput)
must be the last argument off
.
obsBound
的订户可以定义将处理cBInput
的功能 (其作用与cB(cBInput)
)。 应用的约定是,回调函数cB(cBInput)
必须是f
的最后一个参数。
It is probably easier to understand the mechanism looking at the following diagram:
查看下图可能更容易理解该机制:
The starting point, the function f(x, cb)
is the same in the two cases. The result (what is printed on the console) is the same in the two cases.
起点,函数f(x, cb)
在两种情况下相同。 在两种情况下,结果(控制台上打印的内容)相同。
What is different is how the result is obtained. In the first case the result is determined by the callback function passed as input. In the second case it is determined by the function defined by the subscriber.
不同的是如何获得结果。 在第一种情况下,结果由作为输入传递的回调函数确定。 在第二种情况下,它由用户定义的功能确定。
Another way of considering how bindCallback
works is to look at the transformation it performs, as illustrated in the diagram below.
考虑bindCallback
工作方式的另一种方法是查看它执行的转换,如下图所示。
The first argument of f
becomes the value passed to the new function fBound
. The arguments used as parameters of the callback cb
become the values emitted by the new Observable returned by fBound
.
f
的第一个参数成为传递给新函数fBound
的值。 用作回调cb
参数的参数成为fBound
返回的新Observable发出的值。
bindNodeCallback
is a variation of bindCallback
based on the convention that the callback function has an error parameter as the first parameter, along with the Node.js convention fs.readFile(err, cb)
.
bindNodeCallback
是bindNodeCallback
的变体, bindCallback
基于以下约定:回调函数将error参数作为第一个参数,以及Node.js约定fs.readFile(err, cb)
。
通过非回调函数创建可观察对象 (Create Observables from non-callback functions)
bindNodeCallback
has been designed to work with functions which expect a callback as the last argument of their input, but we can make it work also with other functions.
bindNodeCallback
旨在与希望将回调作为其输入的最后一个参数的函数一起使用,但是我们可以使其与其他函数一起使用。
Let’s consider the standard Node.js function readLine
. This is a function used to read files line by line. The following example shows as it works:
让我们考虑标准的Node.js函数readLine
。 此功能用于逐行读取文件。 以下示例显示了它的工作原理:
Each line read is pushed into the lines
array. When the file is completely read, the function processLinesCb
is called.
每个读取的lines
被推入lines
数组中。 完全读取文件后,将调用函数processLinesCb
。
Imagine now that we define a new function,_readLines
, which wraps the logic defined above as shown by the following snippet:
现在想象一下,我们定义了一个新函数_readLines
,它包装了上面定义的逻辑,如以下代码片段所示:
Once all lines are read, they are processed by the function processLinesCb
, which is the last input parameter of _readLines
. _readLines
is therefore a function that can be treated by bindCallback
. Through this trick we can transform the Node.js function fs.readLine
into an Observable using the usual bindCallback
function as follows:
一旦所有的行被读取,它们被函数处理processLinesCb
,这是最后一个输入参数_readLines
。 因此, _readLines
是可以由bindCallback
处理的bindCallback
。 通过此技巧,我们可以使用通常的bindCallback
函数将Node.js函数fs.readLine
转换为Observable,如下所示:
结论 (Conclusion)
Asynchronous non-blocking processing is complex by nature. Our minds are used to think sequentially — this is true at least for those of us who started coding few years ago. We often find it challenging to reason about what is really happening in these environments. The callback-hell is just around the corner.
异步非阻塞处理本质上是复杂的。 我们的思维习惯于顺序思考-至少对于几年前开始编写代码的我们来说,这是正确的。 我们常常发现很难推理出这些环境中实际发生的事情。 回调地狱指日可待。
Promises and Futures have simplified some of the most frequent cases such as ‘one time’ asynchronous events, the ‘request now — respond later’ scenario typical of HTTP requests.
Promise and Futures简化了一些最常见的情况,例如HTTP请求中典型的“一次性”异步事件,“立即请求-稍后响应”方案。
If we move from ‘one time’ events to ‘event streams’ Promises start showing some limitations. In such cases we may find ReactiveX and Observables a very powerful tool.
如果我们从“一次”事件转移到“事件流”,则承诺将开始显示出一些局限性。 在这种情况下,我们可能会发现ReactiveX和Observables是一个非常强大的工具。
如所承诺的那样:基于Promise的用例实现 (As promised: the Promise-based implementation of our use case)
This is an implementation of the same use case based on Promises:
这是基于Promises的同一用例的实现:
const promises = new Array<Promise>();
readDirPromise(dir)
.then(fileList => {
for (const file of fileList) {promises.push(
readFilePromise(file)
.then(file_content => transform(file_content))
.then(file => writeLogPromise(file))
);
}
return promises;
}
.then(promises => Promise.all(promises))
.then(() => console.log(‘I am done’))
.catch(err => { // manage error })
翻译自: https://www.freecodecamp.org/news/rxjs-and-node-8f4e0acebc7c/
rxjs react