【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad

本文是Rxjs 响应式编程-第二章:序列的深入研究这篇文章的学习笔记。

示例代码托管在:

更多博文:《大史住在大前端》目录

【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad

一. 划重点

文中使用到的一些基本运算符:

map-映射

filter-过滤

reduce-有限列聚合

scan-无限列聚合

flatMap-拉平操作(重点)

catch-捕获错误

retry-序列重试

from-生成可观测序列

range-生成有限的可观测序列

interval-每隔指定时间发出一次顺序整数

distinct-去除出现过的重复值

建议自己动手尝试一下,记住就可以了,有过lodash使用经验的开发者来说并不难。

二. flatMap功能解析

原文中在http请求拿到获取到数据后,最初使用了forEach实现了手动流程管理,于是原文提出了优化设想,试图探究如何依赖响应式编程的特性将手动的数据加工转换改造为对流的转换,好让最终的消费者能够拿到直接可用的数据,而不是得到一个响应后手动进行很多后处理。在代码层面需要解决的问题就是,如何在不使用手动遍历的前提下将一个有限序列中的数据逐个发给订阅者,而不是一次性将整个数据集发过去。

假设我们现在并不知道有flatMap这样一个可以使用的方法,那么先来做一些尝试:

var quakes = Rx.Observable.create(function(observer) { //模拟得到的响应流 var response = { features:[{ earth:1 },{ earth:2 }], test:1 } /* 最初的手动遍历代码 var quakes = response.features; quakes.forEach(function(quake) { observer.onNext(quake); });*/ observer.onNext(response); }) //为了能将features数组中的元素逐个发送给订阅者,需要构建新的流 .map(dataset){ return Rx.Observable.from(dataset.features) }

当我们订阅quakes这个事件流的时候,每次都会得到另一个Observable,它是因为数据源经过了映射变换,从数据变成了可观测对象。那么为了得到最终的序列值,就需要再次订阅这个Observable,这里需要注意的是可观测对象被订阅前是不启动的,所以不用担心它的时序问题。

quakes.subscribe(function(data){ data.subscribe(function(quake){ console.log(quake); }) });

如果将Observable看成一个盒子,那么每一层盒子只是实现了流程控制功能性的封装,为了取得真正需要使用的数据,最终的订阅者不得不像剥洋葱似的通过subscribe一层层打开盒子拿到最里面的数据,这样的封装性对于数据在流中的传递具有很好的隔离性,但是对最终的数据消费者而言,却是一件很麻烦的事情。

这时flatMap运算符就派上用场了,它可以将冗余的包裹除掉,从而在主流被订阅时直接拿到要使用的数据,从大理石图来直观感受一下flatMap:

【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad

乍看之下会觉得它和merge好像是一样的,其实还是有一些区别的。merge的作用是将多个不同的流合并成为一个流,而上图中A1,A2,A3这三个流都是当主流A返回数据时新生成的,可以将他们想象为A的支流,如果你想在支流里捞鱼,就需要在每个支流里布网,而flatMap相当于提供了一张大网,将所有A的支流里的鱼都给捞上来。

所以在使用了flatMap后,就可以直接在一级订阅中拿到需要的数据了:

var quakes = Rx.Observable.create(function(observer) { var response = { features:[{ earth:1 },{ earth:2 }], test:1 } observer.onNext(response); }).flatMap((data)=>{ return Rx.Observable.from(data.features); }); quakes.subscribe(function(quake) { console.log(quake) }); 三. flatMap的推演 3.1 函数式编程基础知识回顾

如果本节的基本知识你尚不熟悉,可以通过javascript基础修炼(8)——指向FP世界的箭头函数这篇文章来简单回顾一下函数式编程的基本知识,然后再继续后续的部分。

/*map运算符的作用 *对所有容器类而言,它相当于打开容器,进行操作,然后把容器再盖上。 *Container在这里只是一个抽象定义,为了看清楚它对于容器中包含的值意味着什么。 *你会发现它其实就是Observable的抽象原型。 */ Container.prototype.map = function(f){ return Container.of(f(this.__value)) } //基本的科里化函数 var curry = function(fn){ args = [].slice.call(arguments, 1); return function(){ [].push.apply(args, arguments); return fn.apply(this, args); } } //map pointfree风格的map运算符 var map = curry(function(f, any_functor_at_all) { return any_functor_at_all.map(f); }); /*compose函数组合方法 *运行后返回一个新函数,这个函数接受一个参数。 *函数科里化的基本应用,也是函数式编程中运算管道构建的基本方法。 */ var compose = function (f, g) { return function (x) { return f(g(x)); } }; /*IO容器 *一个简单的Container实现,用来做流程管理 *这里需要注意,IO实现的作用是函数的缓存,且总是返回新的IO实例 *可以看做一个简化的Promise,重点是直观感受一下它作为函数的 *容器是如何被使用的,对于理解Observable有很大帮助 */ var IO = function(f) { this.__value = f; } IO.of = function(x) { return new IO(function() { return x; }); } IO.prototype.map = function(f) { return new IO(compose(f, this.__value)); }

如果上面的基本知识没有问题,那么就继续。

3.2 从一个容器的例子开始

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygjdw.html