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

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

响应式函数式编程

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

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

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 并在有新值的时候通知它的能力。我们可以想象下监听器是这样的:

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 相当于持续惰性的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏编程

C语言嵌入式系统编程修炼之性能优化

这是我13年前创作和发表在互联网上的文章,这么多年过去了,这篇文章仍然在到处传播。现在贴回Linuxer公众号。 全文目录: C语言嵌入式系统编程修炼之道——背...

1837
来自专栏小狼的世界

短链接算法收集与分析

一般来说,第三步是我们比较头疼的,如何将一个长的URL字符串,映射成一个较短的字符串呢。我总结了三种办法:

901
来自专栏腾讯数据库技术

玩转MyRocks/RocksDB--STATISTICS与后台线程篇

1592
来自专栏青玉伏案

设计模式(九): 从醋溜土豆丝和清炒苦瓜中来学习"模板方法模式"(Template Method Pattern)

今天是五.四青年节,祝大家节日快乐。看着今天这标题就有食欲,夏天到了,醋溜土豆丝和清炒苦瓜适合夏天吃,好吃不上火。这两道菜大部分人都应该吃过,特别是醋溜土豆丝,...

1649
来自专栏JetpropelledSnake

ELK学习笔记之ElasticSearch的索引详解

Elasticsearch是通过Lucene的倒排索引技术实现比关系型数据库更快的过滤。特别是它对多条件的过滤支持非常好,比如年龄在18和30之间,性别为女性这...

795
来自专栏杨建荣的学习笔记

两个关于权限设置的问题思考

最近这两天做动态菜单和权限校验,想到了两个有意思的问题。 第一个是对于一个用户的操作权限,无非就是这四个方面,增删改查。 如果通过字母来标识,可能就是增(I)删...

3427
来自专栏calmound

网络流—最大流(Edmond-Karp算法)

 网络流看了两天,终于有了一点眉目,也拿模版A了道题目,通过对于模版代码的调试也真正了解了ek算法的用途了。  想好好写下总结都不让人顺心,写到一半网站死了,又...

3626
来自专栏何俊林

Android Multimedia框架总结(十)Stagefright框架之音视频输出过程

前言:上篇文中最后介绍了数据解码放到Buffer过程,今天分析的是stagefright框架中音视频输出过程: 先看下今天的Agenda: 一张图回顾数据处理...

2068
来自专栏Golang语言社区

一起用golang之Go程序的套路

系统性地介绍golang基础的资料实在太多了,这里不再一一赘述。本文的思路是从另一个角度来由浅入深地探究下Go程序的套路。毕竟纸上得来终觉浅,所以,能动手就不要...

1562
来自专栏腾讯云数据库(TencentDB)

【腾讯云CDB】教你玩转MyRocks/RocksDB—STATISTICS与后台线程篇

本文将介绍 SHOW ENGINE ROCKSDB STATUS 中关于 STATISTICS 统计值与后台线程的实现原理。

9687

扫码关注云+社区