一、什么是FRP

FRP全称叫函数响应式编程(Functional Reactive Programming),是一种编程模型,简单点说可以把若干不同数据在不做任何修改的情况下转换成新的数据,一直交替直到符合系统需求的数据。

比如:百度搜索框的自动补全,input输入下拉列表实时更新。其中input输入我们可以理解为最开始的数据,然后请求服务端,返回JSON数据,最后再转换后HTML写入DOM中。这是典型的一种外部输入数据(信号),数据(信号)的变化通过事件通知,最后经过一系列响应最后反馈给DOM。

这种FRP编程范式,已经有很多第三方类库,比如:ReactiveX(JS/.NET/Java/Swift/Scala)、Flapjax。而Angular2也把Rx(即ReactiveX JS版本)作为依赖关系。

二、一个Suggest示例

这是一个查询维基百科词语的Suggest(实在找不到百度有提供API),虽然很普通很常见,但我们还是画个流程图,以便于我们更清晰需要做什么。

FRP-Suggest

用户通过input输入词语,向维基API服务端请求,服务端处理完成后返回结果,再根据结果转换成相应的HTML代码,最后插入到DOM中;整个过程周而复始。

其实我们会发现整个过程的触发实际都是在一个时间线上,通过这一时间线对数据进行过滤、转换等等,而最后将数据(input输入的单词)转换成一个DOM列表。这就是FRP思想。而往往在语言实现上我们会以事件流的行式来表现。

那么我们来看一下代码是怎么写(完整代码,采用ES6编写,所以最好在FF下查看)。

var ob = Rx.Observable.fromEvent(qEl, 'keyup') // 创建观察手
  .map(e => e.target.value) // 
  .filter(text => text.length > 2) // 过滤
  .throttle(200) // 节流阀,防止按键过快倒置请求过频
  .distinctUntilChanged() // 忽略和上一次相同的操作,以免浪费资源
  .flatMapLatest(searchWikipedia) // 将当前的数据通过 searchWikipedia 映射成为一个新的数据
  .subscribe(data => { // 创建订阅方法,即拉取最新数据。
    var res = data[1];
    resultElr.empty();
    $.each(res, (_, value) => $('<li>' + value + '</li>').appendTo(resultElr));
  }, error => {
    resultElr.empty();
    $('<li>Error: ' + error + '</li>').appendTo(resultElr);
  });

// 这里必须返回一个带promise的函数
function searchWikipedia(term) {
    return $.ajax({
        url: 'http://en.wikipedia.org/w/api.php',
        dataType: 'jsonp',
        data: {
            action: 'opensearch',
            format: 'json',
            search: term
        }
    }).promise();
}

三、事件流处理

由上面的代码,可以了解FRP的几种常见事件流处理方法,而这些方法无疑就是方便对事件流的组合和转换。

1、映射

代码中 map(function)flatMapLatest(function),将事件流通过function映射成一个新的事件流。mapflatMapLatest 的区别在于,后者我们可以提供一个带有Promise对象,而最后被映射的是具体的值,而非带有值具体的值的Promise对象,恩,有点绕。以前面的示例,假设我们使用 map 来操作,那么我们需要这样:

ob.map(function(word) {
    return Rx.Observable.fromPromise(searchWikipedia(word));
});

2、过滤

代码中 filter(function)distinctUntilChanged(),事件流通过function判断结果为 true 的作为一个新的事件流。distinctUntilChanged 我的理解是比较,那么他也是相当于过滤中的一类吧。

使用中常见而代码又未提及的还有很多。

3、合并

merge(es1, es2, ...),把事件流和es1…3事件流合并成一个新的事件流。

4、条件

Rx.js提供了非常多的条件表达式,比如:

skip(n),跳过n个事件流,skip在rx当中还有很多个辅助版本。

四、行为能力

1、时间

FRP是以时间线来处理数据的,所以我时间也变成了最基本的一个。如上面的 throttle 表示200毫秒才会更新一次,换句话在这200毫秒发生的多个事件流最后都以最后一次的事件流为准。

正因为这种以时间线来处理数据,完全可以遏止像这种增量式搜索的资源浪费,就变得非常的简单。

2、计算

像一些加减乘除以及一些常用的统计 sum()min()max() 等等。

Rx.js还提供非常非常多的一些方便对事件流操作的方法,有兴趣可以看:,见:Observable Instance Methods

五、异步HTTP

我们再看一个异步请求HTTP的示例,当然如果只是一次请求看不出RX有什么特效。但假设我们需要同时两次请求,先查询列表,再根据结果再请求详细数据,这是多么觉见的场景。以下是通过常用的jQuery来实现。

$.get('/data1')
 .done(function(res) {
    $.get(res.data, function(res) {
      // process
    });
  });

以上已经尽可能短的描述两次请求的方式,看起来很干净,好像并无多余??等等,怎么看都怎么觉得奇怪,这好像又进入callback的深渊。那么看看FRP里面是怎么实现的:


var result2El = $('#result2'), source = Rx.Observable.fromEvent($('#demo2'), 'click'); source .map(() => 'node' ) .map(function(data) { result2El.html('开始查询带有[' + data + ']的词条...'); return data; }) .flatMapLatest(searchWikipedia) .map(function(res) { var data = res[1]; result2El.html('开始查询[' + data[0] + ']词条的内容...'); return data[0]; }) .flatMapLatest(searchWikipediaContent) .subscribe(data => { result2El.html('结果:' + JSON.stringify(data)); }, error => { result2El.html('错误'); });

怎么样,多么符合线性逻辑思考思维。对代码感兴趣点这

六、为什么需要FRP

对于FRP这种编程模型来讲,他是通过事件流的方式把数据进行重组,同样一个业务我们只需要依赖事件的组装,而不再过多的关注业务细节问题;转向把细节交给各种子函数来处理。其实我个人理解相当于把模块化开发思维更细致的转化到代码层面。

对于这种带有时间线性的开发方式,也非常符合程序员的线性表达业务逻辑思维方式。

在新版的Angular2,其中事件处理也采用这种方式。而对于FRP实现的框架而言,最重要的是有序的事件传播。如前面我们创建一个观察手,而这个观察手监听着 keyup 事件,一但事件发生变化,立即执行变通过一系列的事件流处理最终以DOM的形式展示。

最后

Angular2把RxJs做为依赖库并存着,虽然FRP有实现有很多,我也不知道Google和M$什么时候这么亲密,因为Rx也是M$提供的,而Angualr2的首选开发语言也是TypeScript。而市面上还存在其他很多FRP库(例如:Flapjax、Fran、Yampa),也许提供的API接口各有不同,但他们的FRP思想是一样的。

Rx不光是有JavaScript中使用,前面已经有所提及,它可以在.Net、Java等等后端开发上使用。