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

示例代码托管在:http://www.github.com/dashnowords/blogs

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

一. 划重点

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

  • 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:

乍看之下会觉得它和merge好像是一样的,其实还是有一些区别的。merge的作用是将多个不同的流合并成为一个流,而上图中A1A2A3这三个流都是当主流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 从一个容器的例子开始

现在来实现这样一个功能,读入一个文件的内容,将其中的a字符全部换成b字符,接着存入另一个文件,完成后在控制台输出一个消息,为了更明显地看到数据容器的作用,我们使用同步方法并将其包裹在IO容器中,然后利用函数式编程:

var fs = require('fs');

//读取文件
var readFile = (filename)=>IO.of(fs.readFileSync(filename,'utf-8')); //转换字符
var transContent = (content)=>IO.of((content)=>content.replace('a','b')); //写入字符串
var writeFile = (content)=>IO.of(fs.writeFileSync('dest.txt',content));

当具体的函数被IO容器包裹起来而实现延迟执行的效果时,就无法按原来的方式使用compose( )运算符直接对功能进行组合,因为readFile函数运行时的输出结果(一个io容器实例)和transContent函数需要的参数类型(字符串)不再匹配,在不修改原有函数定义的前提下,函数式编程中采用的做法是使用map操作符来预置一个参数:

/*
*map(transContent)是一个高阶函数,它的返回函数就可以接收一个容器实例,
*并对容器中的内容执行map操作。
*/
var taskStep12 = compose(map(transContent), readFile);

这里比较晦涩,涉及到很多功能性函数的嵌套,建议手动推导一下taskStep12这个变量的值,它的结构是这样一种形式:

io{
__value:io{
__value:someComposedFnExpression
}
}

如果试图一次性将所有的步骤组合在一起,就需要采用下面的形式:

var task = compose(map(map(writeFile)),map(transContent),readFile);
//组合后的task形式就是
//io{io{io{__value:someComposedFnExpression}}}

问题已经浮出水面了,每多加一个针对容器操作的步骤,书写时就需要多包裹一层map,而运行时就需要多进入一层才能触及组合好的可以实现真正功能的函数表达式,真的是很麻烦。

【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad的更多相关文章

  1. 【响应式编程的思维艺术】 (2)响应式Vs面向对象

    目录 一. 划重点 二. 面向对象编程实例 2.1 动画的基本编程范式 2.2 参考代码 2.3 小结 三. 响应式编程实现 四. 差异对比 4.1 编程理念差异 4.2 编程体验差异 4.3 数学思 ...

  2. 【响应式编程的思维艺术】 (5)Angular中Rxjs的应用示例

    目录 一. 划重点 二. Angular应用中的Http请求 三. 使用Rxjs构建Http请求结果的处理管道 3.1 基本示例 3.2 常见的操作符 四. 冷热Observable的两种典型场景 4 ...

  3. 【响应式编程的思维艺术】 (1)Rxjs专题学习计划

    目录 一. 响应式编程 二. 学习路径规划 一. 响应式编程 响应式编程,也称为流式编程,对于非前端工程师来说,可能并不是一个陌生的名词,它是函数式编程在软件开发中应用的延伸,如果你对函数式编程还没有 ...

  4. 浅谈Spring 5的响应式编程

    这篇使用Spring 5进行响应式编程的入门文章展示了你现在可以使用的一些新的non-blocking, asynchronous.感谢优锐课老师给予的指导! 近年来,由于响应式编程能够以声明性的方式 ...

  5. Rx系列---响应式编程

    Rx是ReactiveX的简称,翻译过来就是响应式编程 首先要先理清这么一个问题:Rxjava和我们平时写的程序有什么不同.相信稍微对Rxjava有点认知的朋友都会深深感受到用这种方式写的程序和我们一 ...

  6. iOS响应式编程:ReactiveCocoa vs RxSwift 选谁好

    转载: iOS响应式编程:ReactiveCocoa vs RxSwift 选谁好 内容来自stack overflow的一个回答:ReactiveCocoa vs RxSwift – pros an ...

  7. 响应式编程笔记三:一个简单的HTTP服务器

    # 响应式编程笔记三:一个简单的HTTP服务器 本文我们将继续前面的学习,但将更多的注意力放在用例和编写实际能用的代码上面,而非基本的APIs学习. 我们会看到Reactive是一个有用的抽象 - 对 ...

  8. 1小时让你掌握响应式编程,并入门Reactor

    我看同步阻塞 “你知道什么是同步阻塞吗”,当然知道了.“那你怎么看它呢”,这个... 在同步阻塞的世界里,代码执行到哪里,数据就跟到哪里.如果数据很慢跟不上来,代码就停在那里等待数据的到来,然后再带着 ...

  9. Spring 5 响应式编程

    要点 Reactor 是一个运行在 Java8 之上的响应式流框架,它提供了一组响应式风格的 API 除了个别 API 上的区别,它的原理跟 RxJava 很相似 它是第四代响应式框架,支持操作融合, ...

随机推荐

  1. $.each()和$().each(),以及forEach()的用法

    1.forEach() 是JS遍历数组的方法 var arr=[1,2,3]; arr.forEach(function(val,index,arr){ // var 为数组中当前的值 // inde ...

  2. tyflow雨滴在物体上滑落测试

    http://docs.tyflow.com/download/

  3. 关于height、offsetheight、clientheight、scrollheight、innerheight、outerheight的区别

    二.也是平时经常用到的offsetheight 它返回的高度是内容高+padding+边框,但是注意哦,木有加margin哦,当然一般也木有啥需要把margin加进去的,以上代码为例,结果显示上图h2 ...

  4. protobuf是什么?

      ProtoBuf (Google Protocol Buffer)是由google公司用于数据交换的序列结构化数据格式,具有跨平台.跨语言.可扩展特性,类型于常用的XML及JSON,但具有更小的传 ...

  5. Android中RadioGroup的初始化和简单的使用

    一简介: RadioGroup作为一个单选按钮组,可以设置为性别选择男或则女,地址选择等等,作为一个android入门级选手,就简单的说一下RadioGroup组中RadioButton的布局和初始化 ...

  6. java发送短信验证码

    业务: 手机端点击发送验证码,请求发送到java服务器端,由java调用第三方平台(我们使用的是榛子云短信http://smsow.zhenzikj.com)的短信接口,生成验证码并发送. SDK下载 ...

  7. vue-router路径计算问题

    简书原文 昨天刚刚发表了一个前端跨域新方案尝试,今天在开发中就遇到的了问题. 起因 前端使用的是vue-router组件的history模式,但是由于我们的整个页面都是从static(静态资源站)lo ...

  8. 升讯威微信营销系统开发实践:微信接口的 .NET 封装

    GitHub:https://github.com/iccb1013/Sheng.WeixinConstruction因为个人精力时间有限,不会再对现有代码进行更新维护,不过微信接口比较稳定,经测试至 ...

  9. 亿级SQL Server运维的最佳实践PPT分享

    这次分享是我在微软的一次分享,关于SQL Server运维最佳实践的部分,由于受众来自不同背景,因此我让分享在一个更加抽象的角度进行,PPT分享如下: 点击这里进行下载

  10. oracle 分析函数和开窗函数

    最近遇到一个需求,将查询出的数据按照地区分组,随机取出每个区域的2条数据,这里用到了oracle的分析和开窗函数: 最终写出的sql如下: select * from (select region,r ...