翻译连载 | 第 10 章:异步的函数式(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇

关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。通过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,但愿能够帮助你们在学习函数式编程的道路上走的更顺畅。比心。前端

译者团队(排名不分前后):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry萝卜vavd317vivaxy萌萌zhouyaogit

第 10 章:异步的函数式(下)

响应式函数式编程

为了理解如何在2个值之间建立和使用惰性的映射,咱们须要去抽象咱们对列表(数组)的想法。github

让咱们来想象一个智能的数组,不仅是简单地得到值,仍是一个懒惰地接受和响应(也就是“反应”)值的数组。考虑下:web

var a = new LazyArray();

var b = a.map( function double(v){
	return v * 2;
} );

setInterval( function everySecond(){
	a.push( Math.random() );
}, 1000 );
复制代码

至此,这段代码的数组和普通的没有什么区别。惟一不一样的是在咱们执行 map(..) 来映射数组 a 生成数组 b 以后,定时器在 a 里面添加随机的值。编程

可是这个虚构的 LazyArray 有点不一样,它假设了值能够随时的一个一个添加进去。就像随时能够 push(..) 你想要的值同样。能够说 b 就是一个惰性映射 a 最终值的数组。小程序

此外,当 a 或者 b 改变时,咱们不须要确切地保存里面的值,这个特殊的数组将会保存它所需的值。因此这些数组不会随着时间而占用更多的内存,这是 惰性数据结构和懒操做的重要特色。事实上,它看起来不像数组,更像是buffer(缓冲区)。微信小程序

普通的数组是积极的,因此它会立马保存全部它的值。"惰性数组" 的值则会延迟保存。数组

因为咱们不必定要知道 a 何时添加了新的值,因此另外一个关键就是咱们须要有去监听 b 并在有新值的时候通知它的能力。咱们能够想象下监听器是这样的:promise

b.listen( function onValue(v){
	console.log( v );
} );
复制代码

b 是反应性的,由于它被设置为当 a 有值添加时进行反应。函数式编程操做当中的 map(..) 是把数据源 a 里面的全部值转移到目标 b 里。每次映射操做都是咱们使用同步函数式编程进行单值建模的过程,可是接下来咱们将让这种操做变得能够响应式执行。缓存

注意: 最经常使用到这些函数式编程的是响应式函数式编程(FRP)。我故意避开这个术语是由于一个有关于 FP + Reactive 是否真的构成 FRP 的辩论。咱们不会全面深刻了解 FRP 的全部含义,因此我会继续称之为响应式函数式编程。或者,若是你不会感受那么困惑,也能够称之为事件机制函数式编程。

咱们能够认为 a 是生成值的而 b 则是去消费这些值的。因此为了可读性,咱们得从新整理下这段代码,让问题归结于 生产者消费者

// 生产者:

var a = new LazyArray();

setInterval( function everySecond(){
	a.push( Math.random() );
}, 1000 );


// **************************
// 消费者:

var b = a.map( function double(v){
	return v * 2;
} );

b.listen( function onValue(v){
	console.log( v );
} );
复制代码

a 是一个行为本质上很像数据流的生产者。咱们能够把每一个值赋给 a 看成一个事件map(..) 操做会触发 b 上面的 listen(..) 事件来消费新的值。

咱们分离 生产者消费者 的相关代码,是由于咱们的代码应该各司其职。这样的代码组织能够很大程度上提升代码的可读性和维护性。

声明式的时间

咱们应该很是谨慎地讨论如何介绍时间状态。具体来讲,正如 promise 从单个异步操做中抽离出咱们所担忧的时间状态,响应式函数式编程从一系列的值/操做中抽离(分割)了时间状态。

a (生产者)的角度来讲,惟一与时间相关的就是咱们手动调用的 setInterval(..) 循环。但它只是为了示范。

想象下 a 能够被绑定上一些其余的事件源,好比说用户的鼠标点击事件和键盘按键事件,服务端来的 websocket 消息等。在这些状况下,a 不必关注本身的时间状态。每当值准备好,它就只是一个与值链接的无时态管道。

b (消费者)的角度来讲,咱们不用知道或者关注 a 里面的值在什么时候何地来的。事实上,全部的值都已经存在。咱们只关注是否不管什么时候都能取到那些值。或者说,map(..) 的转换操做是一个无时态(惰性)的建模过程。

时间ab 之间的关系是声明式的,不是命令式的。

以 operations-over-time 这种方式来组织值可能不是颇有效。让咱们来对比下相同的功能如何用命令式来表示:

// 生产者:

var a = {
	onValue(v){
		b.onValue( v );
	}
};

setInterval( function everySecond(){
	a.onValue( Math.random() );
}, 1000 );


// **************************
// 消费者:

var b = {
	map(v){
		return v * 2;
	},
	onValue(v){
		v = this.map( v );
		console.log( v );
	}
};
复制代码

这彷佛很微妙,但这就是存在于命令式版本的代码和以前声明式的版本之间一个很重要的不一样点,除了 b.onValue(..) 须要本身去调用 this.map(..) 以外。在以前的代码中, ba 当中去拉取,可是在这个代码中,a 推送给 b。换句话说,把 b = a.map(..) 替换成 b.onValue(v)

在上面的命令式代码中,以消费者的角度来讲它并不清楚 v 从哪里来。此外命令式强硬的把代码 b.onValue(..) 夹杂在生产者 a 的逻辑里,这有点违反了关注点分离原则。这将会让分离生产者和消费者变得困难。

相比之下,在以前的代码中,b = a.map(..) 表示了 b 的值来源于 a ,对于如同抽象事件流的数据源 a,咱们不须要关心。咱们能够 确信 任何来自于 ab 里的值都会经过 map(..) 操做。

映射以外的东西

为了方便,咱们已经说明了经过随着时间一次一次的用 map(..) 来绑定 ab 的概念。其实咱们许多其余的函数式编程操做也能够作到这种效果。

思考下:

var b = a.filter( function isOdd(v) {
	return v % 2 == 1;
} );

b.listen( function onlyOdds(v){
	console.log( "Odd:", v );
} );
复制代码

这里能够看到 a 的值确定会经过 isOdd(..) 赋值给 b

即便是 reduce(..) 也能够持续的运行:

var b = a.reduce( function sum(total,v){
	return total + v;
} );

b.listen( function runningTotal(v){
	console.log( "New current total:", v );
} );
复制代码

由于咱们调用 reduce(..) 是没有给具体 initialValue 的值,不管是 sum(..) 或者 runningTotal(..) 都会等到有 2 个来自 a 的参数时才会被调用。

这段代码暗示了在 reduction 里面有一个 内存空间, 每当有新的值进来的时候,sum(..) 才会带上第一个参数 total 和第二个参数 v被调用。

其余的函数式编程操做会在内部做用域请求一个缓存区,好比说 unique(..) 能够追踪每个它访问过的值。

Observables

但愿如今你能够察觉到响应式,事件式,类数组结构的数据的重要性,就像咱们虚构出来的 LazyArray 同样。值得高兴的是,这类的数据结构已经存在的了,它就叫 observable。

注意: 只是作些假设(但愿):接下来的讨论只是简要的介绍 observables。这是一个须要咱们花时间去探究的深层次话题。可是若是你理解本文中的轻量级函数式编程,而且知道如何经过函数式编程的原理来构建异步的话,那么接着学习 observables 将会变得驾轻就熟。

如今已经有各类各样的 Observables 的库类, 最出名的是 RxJS 和 Most。在写这篇文章的时候,正好有一个直接向 JS 里添加 observables 的建议,就像 promise。为了演示,咱们将用 RxJS 风格的 Observables 来完成下面的例子。

这是咱们一个较早的响应式的例子,可是用 Observables 来代替 LazyArray

// 生产者:

var a = new Rx.Subject();

setInterval( function everySecond(){
	a.next( Math.random() );
}, 1000 );


// **************************
// 消费者:

var b = a.map( function double(v){
	return v * 2;
} );

b.subscribe( function onValue(v){
	console.log( v );
} );
复制代码

在 RxJS 中,一个 Observer 订阅一个 Observable。若是你把 Observer 和 Observable 的功能结合到一块儿,那就会获得一个 Subject。所以,为了保持代码的简洁,咱们把 a 构建成一个 Subject,因此咱们能够调用它的 next(..) 方法来添加值(事件)到他的数据流里。

若是咱们要让 Observer 和 Observable 保持分离:

// 生产者:

var a = Rx.Observable.create( function onObserve(observer){
	setInterval( function everySecond(){
		observer.next( Math.random() );
	}, 1000 );
} );
复制代码

在这个代码里,a 是 Observable,毫无疑问,observer 就是独立的 observer,它能够去“观察”一些事件(好比咱们的setInterval(..)循环),而后咱们使用它的 next(..) 方法来发送一些事件到 observable a 的流里。

除了 map(..),RxJS 还定义了超过 100 个能够在有新值添加时才触发的方法。就像数组同样。每一个 Observable 的方法都会返回一个新的 Observable,意味着他们是链式的。若是一个方法被调用,则它的返回值应该由输入的 Observable 去返回,而后触发到输出的 Observable里,不然抛弃。

一个链式的声明式 observable 的例子:

var b =
	a
	.filter( v => v % 2 == 1 )      // 过滤掉偶数
	.distinctUntilChanged()         // 过滤连续相同的流
	.throttle( 100 )                // 函数节流(合并100毫秒内的流)
	.map( v = v * 2 );              // 变2倍

b.subscribe( function onValue(v){
	console.log( "Next:", v );
} );
复制代码

注意: 这里的链式写法不是必定要把 observable 赋值给 b 和调用 b.subscribe(..) 分开写,这样作只是为了让每一个方法都会获得一个新的返回值。一般,subscribe(..) 方法都会在链式写法的最后被调用。

总结

这本书详细的介绍了各类各样的函数式编程操做,例如:把单个值(或者说是一个即时列表的值)转换到另外一个值里。

对于那些有时态的操做,全部基础的函数式编程原理均可以无时态的应用。就像 promise 建立了一个单一的将来值,咱们能够建立一个积极的列表的值来代替像惰性的observable(事件)流的值。

数组的 map(..) 方法会用当前数组中的每个值运行一次映射函数,而后放到返回的数组里。而 observable 数组里则是为每个值运行一次映射函数,不管这个值什么时候加入,而后把它返回到 observable 里。

或者说,若是数组对函数式编程操做是一个积极的数据结构,那么 observable 至关于持续惰性的。

** 【上一章】翻译连载 | 第 10 章:异步的函数式(上)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇 **

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

iKcamp官网:www.ikcamp.com 访问官网更快阅读所有免费分享课程:《iKcamp出品|全网最新|微信小程序|基于最新版1.0开发者工具之初中级培训教程分享》。 包含:文章、视频、源代码


2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!