# Hello RxJS
使用 jQuery 实现时间感应用。
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<div>测试对时间的感觉</div>
<button id="hold-me">按住 1s 后松开</button>
<div>你的时间:<span id="hold-time"></span>ms</div>
<div id="rank"></div>
</div>
<script
src="https://code.jquery.com/jquery-1.12.4.js"
integrity="sha256-Qw82 bXyGq6MydymqBxNPYTaUXXq7c8v3CwiYwLLNXU="
crossorigin="anonymous"></script>
<script>
let startTime;
$('#hold-me').mousedown(function() {
startTime = new Date();
});
$('#hold-me').mouseup(function () {
if (startTime) {
const elapsedTime = new Date() - startTime;
startTime = null;
$('#hold-time').text(elapsedTime);
$.ajax(`https://timing-sense-score-board.herokuapp.com/score/${elapsedTime}`)
.done(function (data) {
$('#rank').text(`你超过了 ${data.rank}% 的人`);
});
}
});
</script>
</body>
</html>
使用 RxJS 实现:
代码语言:javascript复制<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<div>测试对时间的感觉</div>
<button id="hold-me">按住 1s 后松开</button>
<div>你的时间:<span id="hold-time"></span>ms</div>
<div id="rank"></div>
</div>
<script src="https://unpkg.com/rxjs@5.4.2/bundles/Rx.min.js"></script>
<script>
const holdMeBtn = document.querySelector('#hold-me');
const mouseDown$ = Rx.Observable.fromEvent(holdMeBtn, 'mousedown');
const mouseUp$ = Rx.Observable.fromEvent(holdMeBtn, 'mouseup');
const holdTime$ = mouseUp$.timestamp().withLatestFrom(
mouseDown$.timestamp(),
(up, down) => up.timestamp - down.timestamp
);
holdTime$.subscribe(ms => {
document.querySelector('#hold-time').innerText = ms;
});
holdTime$.flatMap(ms => Rx.Observable.ajax(`https://timing-sense-score-board.herokuapp.com/score/${ms}`))
.map(e => e.response)
.subscribe(data => {
document.querySelector('#rank').innerText = `你超过了 ${data.rank}% 的人`;
});
</script>
</body>
</html>
RxJS 世界中有一种特殊对象——“流”,也可以叫“数据流”或“Observable对象”。
代表“流”的变量标识符,都是以
$
结尾。
上面 mouseDown 和 mouseUp 都是数据流,分别代表按钮上的 mousedown 事件和 mouseup 事件集合,不光包含已经发生的事件,还包含没有发生的鼠标事件。对数据流一视同仁,这就是数据流的妙处。
“流”可以通过多种方法创造出来,mouseDown 和 mouseUp 通过 fromEvent 函数从网页的 DOM 元素中获得,holdTime 这个流则是通过 mouseDown 和 mouseUp
流对象中“流淌”的是数据,而通过 subscribe
函数可以添加函数对数据进行操作,上面的代码中,对 holdTime$
对象有两个 subscribe
调用,一个用来更新 DOM,另一个用来调用 API 请求。
在 jQuery 的实现中,有被交叉访问的变量(startTime
),两个不同函数的逻辑相互关联,稍有不慎就会引发 bug ,代码看起来就是一串指令的组合;在RxJS的代码中,没有这样纠缠不清的变量,会发现所有的变量其实都没有“变”,赋值时是什么值,就会一直保持这些值,代码是一个一个函数,每个函数只是对输入的参数做了响应,然后返回结果。
RxJS 引用了两个重要的编程思想:
- 函数式
- 响应式
# 函数式编程
# 什么是函数式编程
强调使用函数来解决问题的一种编程方式。函数式编程对函数的使用有一些特殊的要求,这些要求包括以下几点:
- 声明式
- 纯函数
- 数据不可变性
从语言角度讲,JavaScript 不算一个纯粹意义上的函数式编程语言,但是,JavaScript 中的函数有第一公民的身份,因为函数本身就是一个对象,可以被赋值给一个变量,可以作为参数传递,由此可以很方便地应用函数式编程的许多思想。
把函数式编程看作一种编程思想,即使语言本身不支持一些特性,依然可以应用这样的编程思想,用于提高代码的质量。
JavaScript 如何满足函数式编程的特性需要:
声明式
命名式编程
代码语言:javascript复制function double(arr) {
const results = [];
for (let i = 0; i < arr.length; i ) {
results.push(arr[i] * 2);
}
return results;
}
function addOne (arr) {
const results = [];
for (let i = 0; i < arr.length; i ) {
results.push(arr[i] 1);
}
return results;
}
- 世界上很多问题都有相似的模式,命名式编程会使出现很多重复代码
声明式编程
把一个数组映射成另一个数组
代码语言:javascript复制function double (arr) {
return arr.map(function (item) {
return item * 2;
});
}
function addOne(arr) {
return arr.map(function (item) {
return item 1;
});
}
进一步简化
代码语言:javascript复制const double = arr => arr.map(item => item * 2);
const addOne = arr => arr.map(item => item 1);
纯函数
- 纯函数要满足的条件
- 函数的执行过程完全由输入参数决定,不会受除参数之外的任何数据影响
- 函数不会修改任何外部状态,比如修改全局变量或传入的参数对象
- 好处
- 纯函数让代码更加简单,从而更加容易维护,更加不容易产生 bug
- 非常容易写单元测试的
- TDD 的难以推行很大原因是很多项目不遵守函数式编程规范
- 如果被测函数都是纯函数,单元测试可以轻松达到 100% 的代码覆盖率。
- 可能导致函数不纯的原因
- 改变全局变量的值
- 改变输入参数引用的对象
- 读取用户输入,比如调用了
alert
或者confirm
函数 - 抛出一个异常
- 网络输入/输出操作,比如通过 AJAX 调用一个服务器的 API
- 操作浏览器的 DOM
- 本质:做的事情是输入参数到返回结果的一个映射,不要产生副作用
数据不可变
- 需要数据状态发生改变时,保持原有数据不变,产生一个新的数据来体现这种变化
- 不可改变的数据就是 Immutable 数据,它一旦产生,就可以肯定它的值永远不会变,这非常有利于代码的理解
# 函数式编程和面向对象编程的比较
简单说来,面向对象的方法把状态的改变封装起来,以此达到让代码清晰的目的;而函数式编程则是尽量减少变化的部分,以此让代码逻辑更加清晰。
面向对象的思想是把数据封装在类的实例对象中,把数据藏起来,让外部不能直接操作这些对象,只能通过类提供的实例方法来读取和修改这些数据,这样就限制了对数据的访问方式。对于毫无节制任意修改数据的编程方式,面向对象无疑是巨大的进步,因为通过定义类的方法,可以控制对数据的操作。
但是,面向对象隐藏数据的特点,带来了一个先天的缺陷,就是数据的修改历史完全被隐藏了。有人说,面向对象编程提供了一种持续编写烂代码的方式,它让你通过一系列补丁来拼凑程序。
函数式编程中,倾向于数据就是数据,函数就是函数,函数可以处理数据,也是并不像面向对象的类概念一样把数据和函数封在一起,而是让每个函数都不要去修改原有数据(不可变性),而且通过产生新的数据来作为运算结果(纯函数)。
# 响应式编程
# Reactive Extension
Reactive Extension,也叫 ReactiveX,或者简称 Rx,指的是实践响应式编程的一套工具。
An API for asynchronous programming with observable streams.
Rx(包括RxJS)诞生的主要目的虽然是解决异步处理的问题,但并不表示 Rx 不适合同步的数据处理,实际上,使用 RxJS 之后大部分代码不需要关心自己是被同步执行还是异步执行,所以处理起来会更加简单。
# RxJS 是否是函数响应式编程
FRP 包含两个重要元素:
- 指称性(denotative)
- 临时的连续性(temporally continuous)
正统 FRP 认为,一个系统如果能被称为 FRP,除了要有 Functional
和 Reactive
的特点,还必须要能够支持两个事件可以“同时发生”,这就是指称性的要求。总之,按照正统 FRP 的说法,你的系统只有 Functional
和 Reactive
,不能说自己是 FRP。
包括 RxJS 在内的 Rx,到底算不算 FRP ?按照正统 FRP 的观点,Rx 不算,因为 Rx 不满足指称性的要求,在 Rx 的所有实现中,都存在一个局限,就是当两个“流”合并的时候,不能按照 FRP 那样严格处理同时发生的事件。
# 函数响应式编程的优势
RxJS 模型的特点:
- 数据流抽象了很多现实问题
- 网页 DOM 的事件,可以看作为数据流
- 通过 WebSocket 获得的服务器端推送消息可以看作是数据流
- 通过 AJAX 获得服务器端的数据资源也可以看作是数据流,虽然这个数据流中可能只有一个数据
- 网页的动画显示当然更可以看作是一个数据流
- 擅长处理异步操作
- 对数据采用“推”的处理方式,当一个数据产生的时候,被推送给对应的处理函数,这个处理函数不用关心数据是同步产生的还是异步产生的,这样就把开发者从命令式异步处理的枷锁中解放了出来
- 把复杂问题分解成简单问题的组合
- 数据流可能包含复杂的功能,但是可以分解成很多小的部分来实现,实现某一个小功能的函数就是操作符
- 可以说,学习 RxJS 就是学习如何组合操作符来解决复杂问题