本文的目的是说明为什么需要反应式编程,它与函数式编程之间的关系,以及如何使用它编写易于适应新要求的声明性代码。 另外,我想通过一个接近真实的示例来尽可能简短地做到这一点。
执行以下任务:
REST API和终结点/people
提供了某些服务。 对此端点的POST请求将创建一个新实体。 编写一个函数,该函数采用{ name: 'Max' }
形式的对象数组,并使用API创建一组实体(英文,这称为批处理操作)。
让我们以命令式的方式解决此问题:
const request = require('superagent') function batchCreate(bodies) { const calls = [] for (let body of bodies) { calls.push( request .post('/people') .send(body) .then(r => r.status) ) } return Promise.all(calls) }
为了进行比较,让我们以功能样式重写这段代码。 为了简单起见,我们用功能样式表示:
- 使用功能性原语( .map , .filter , .reduce )代替命令式循环( for , while )
- 该代码被组织为“纯”函数-它们仅取决于参数,而不取决于系统状态
功能样式代码:
const request = require('superagent') function batchCreate(bodies) { const calls = bodies.map(body => request .post('/people') .send(body) .then(r => r.status) ) return Promise.all(calls) }
我们得到了一段相同大小的代码,值得一提的是,尚不清楚这一段代码是否比上一段更好。
为了理解为什么第二段代码更好-您需要开始更改代码,想象一下原始任务出现了新要求:
我们调用的服务在一定时间内限制了请求的数量:一秒钟内,一个客户端最多可以执行五个请求。 执行更多请求将导致服务返回429 HTTP错误(请求过多)。
在这一点上,可能值得停止并尝试自己解决问题,%用户名%
让我们以功能代码为基础,并尝试对其进行更改。 “纯”函数式编程的主要问题是它对运行时和输入输出“一无所知”(英语中有一个表示副作用的表达),但实际上我们一直在与它们合作。
为了填补这一空白,Reactive Programming可以提供帮助-一组尝试解决副作用问题的方法。 该范例最著名的实现是使用反应流概念的Rx库。
什么是反应流? 如果时间很短,那么这是一种允许您将功能性原语(.map,.filter,.reduce)应用于随时间分布的对象的方法。
例如,我们通过网络发送一组特定的命令-我们不需要等到获得完整的命令集,就可以将其呈现为响应流,然后就可以使用它了。 这里还有另外两个重要概念:
- 流量可以是无限的,也可以随时间任意分配
- 仅当接收方准备好处理它时,发送方才发送命令(背压)
本文的目的是找到简单的方法,因此,我们将采用Highland库,该库试图解决与Rx相同的问题,但是更容易学习。 里面的想法很简单:让我们以Node.js流为基础,然后将数据从一个Stream传输到另一个Stream。
让我们开始:让我们从一个简单的例子开始-我们将使代码“具有反应性”而不添加任何新功能
const request = require('superagent') const H = require('highland') function batchCreate(bodies) { return H(bodies) .flatMap(body => H(request .post('localhost:3000/people') .send(body) .then(r => r.status) ) ) .collect() .toPromise(Promise) }
您要注意的是:
- H(实体)-我们从数组创建流
- .flatMap及其接受的回调。 这个想法很简单-我们将Promise包装在流构造器中,以便获得具有一个值(或错误)的流。重要的是要了解这是值,而不是Promise。
结果,这给了我们一系列的流-使用flatMap将其平滑为一个可以对其进行操作的值流(谁说的monad?) - .collect-我们需要将一个点中的所有值收集到一个数组中
- .toPromise-将返回给我们Promise,当我们从流中获得价值时,该承诺将实现
现在,让我们尝试实现我们的要求:
const request = require('superagent') const H = require('highland') function batchCreate(bodies) { return H(bodies) .flatMap(body => H(request .post('localhost:3000/people') .send(body) .then(r => r.status) ) ) .ratelimit(5, 1000) .collect() .toPromise(Promise) }
由于背压的概念,在此范例中,这只是一行.ratelimit。 在Rx中,它占用的空间大约相同 。
好了,您的意见很有趣:
- 我能否达到本文开头所声明的结果?
- 使用命令式方法能否获得相似的结果?
- 您对反应式编程感兴趣吗?
PS:在这里您可以找到我的另一篇有关响应式编程的文章