🏠 Functional Programming in
Javascript 主目录第四章 在
Javascript中实现
函数式编程的技术
函数式响应式编程
我们再来建立另一种类型的应用,他的工作方式差不多,都是用函数式编程来响应状态变化。 但是这回应用不会依赖于事件监听。
来想象一下,你在一个新闻媒体公司工作,你的老板让你建立一个web应用来跟踪竞选日的政府竞选结果。 数据会根据地方选区的结果持续流动,所以显示在页面上的结果是具有响应式特征的。然而我们还需跟踪每一个区域的结果, 所以会有多个对象需要追踪。
我们与其建立一个巨大的面向对象的层次结构来为接口建模,不如把它描述为声明式的不可变数据。 我们可以把它转换为纯函数或者半纯函数,半纯函数在必须保持的状态要更新时才产生副作用(理想状况下不会很多)。
我们将使用Bacon.js库,它可以快速开发函数式响应式编程(FRP)的应用。这个应用只在选举日使用, 我们的老板认为它花费的时间应该与之成比例。通过函数式编程和像Bacon.js这样的库,我们将仅需要一半的时间。
不过首先我们需要一些对象来描述选区,比如州、省、区等等。
function Region(name, percent, parties) { // 可变属性: this.name = name; this.percent = percent; // % of precincts reported this.parties = parties; // political parties // 返回一段HTML this.render = function() { var lis = this.parties.map(function(p) { return '' + p.name + ': ' + p.votes + ''; }); var output = '' + this.name + ''; output += '' + lis.join('') + ''; output += 'Percent reported: ' + this.percent; return output; }}function getRegions(data) { return JSON.parse(data).map(function(obj) { return new Region(obj.name, obj.percent, obj.parties); });}var url = 'http://api.server.com/election-data?format=json';var data = jQuery.ajax(url);var regions = getRegions(data);app.container.innerHTML = regions.map(function(r) { return r.render();}).join('');
上面的代码可以满足对静态选举结果列表的展示,然而我们需要一种动态更新选区的方式。是时候来点Bacon和FRP了。
响应式编程
bacon有个函数,Bacon.fromPoll(),用于创建事件流,它让事件成为在一定时间间隔被调用的函数。 还有stream.subscribe()函数让我们可以订阅(subsribe)一个针对这个流的处理函数。由于它是惰性的, 如果没有订阅者(subscriber)流实际上就不去做任何事情。
var eventStream = Bacon.fromPoll(10000, function() { return Bacon.Next;});var subscriber = eventStream.subscribe(function() { var url = 'http://api.server.com/election-data?format=json'; var data = jQuery.ajax(url); var newRegions = getRegions(data); container.innerHTML = newRegions.map(function(r) { return r.render(); }).join('');});
大体上就是把它放到一个每10秒运行一次的循环中,我们的工作完成了。但是这个方法将会不断的ping网络, 并且非常低效。这还不是很函数式。让我们继续深入挖掘一下Bacon.js库。
Beacon里有EventStreams和Properties参数。Properties可以想作是“魔术”变量,它随着事件的响应不断变化。 实际上这不是魔术,因为他们依赖于事件流。属性的不断变化与事件流相关。
Bacon.js里还有一个戏法。Bacon.fromPromise()函数是一种利用promise把事件搞成流的方式。 jQuery从1.5.0版本开始实现了promise接口,所以我们所要做的仅仅是写一个AJAX查询函数, 在异步调用完成时触发事件。每当promise被处理时,他就调用EventStream的订阅者(subscribers)。
var url = 'http://api.server.com/election-data?format=json';var eventStream = Bacon.fromPromise(jQuery.ajax(url));var subscriber = eventStream.onValue(function(data) { newRegions = getRegions(data); container.innerHTML = newRegions.map(function(r) { return r.render(); }).join('');}
promise可以想成是一个初始值;通过Bacon.js,我们可以对初始值惰性地等待。
把它们搁到一块儿
现在我们学完了响应式的内容,最后我们可以用些代码来玩一玩它。
我们可以通过对纯函数的链式调用改变订阅者来做些事情,比如求和或者过滤不想要的结果, 我们只需通过我们所创建的对按钮的onclick()处理函数来完成这些。
// 在函数外创建事件流var eventStream = Bacon.onPromise(jQuery.ajax(url));var subscribe = null;var url = 'http://api.server.com/election-data?format=json';// 未被改变的订阅者$('button#showAll').click(function() { var subscriber = eventStream.onValue(function(data) { var newRegions = getRegions(data).map(function(r) { return new Region(r.name, r.percent, r.parties); }); container.innerHTML = newRegions.map(function(r) { return r.render(); }).join(''); });});// 显示全部选举情况的按钮$('button#showTotal').click(function() { var subscriber = eventStream.onValue(function(data) { var emptyRegion = new Region('empty', 0, [{ name: 'Republican', votes: 0 }, { name: 'Democrat', votes: 0 }]); var totalRegions = getRegions(data).reduce(function(r1, r2) { newParties = r1.parties.map(function(x, i) { return { name: r1.parties[i].name, votes: r1.parties[i].votes + r2.parties[i].votes }; }); newRegion = new Region('Total', (r1.percent + r2.percent) / 2, newParties); return newRegion; }, emptyRegion); container.innerHTML = totalRegions.render(); });});// 只显示报告大于50%的选区$('button#showMostlyReported').click(function() { var subscriber = eventStream.onValue(function(data) { var newRegions = getRegions(data).map(function(r) { if (r.percent > 50) return r; else return null; }).filter(function(r) { return r != null; }); container.innerHTML = newRegions.map(function(r) { return r.render(); }).join(''); });});
它的美丽之处在于:当用户点击按钮时,事件流并没有变化,但是订阅者变化了,这就使所有的工作很平顺。
第四章总结
JavaScript是一门美丽的语言。 它内在美确是因函数式编程而闪耀。它赋予了它良好的扩展性。事实上,打开了函数式的闸门使得头等函数可以做如此之多的事情。 概念互相堆叠,越来越高。
在这章,我们深入了函数式编程的范例,包括函数工厂、柯里化、函数组合等等。我们用这些概念建立了非常模块化的应用。 然后我们展示了如何使用一些函数库利用同样的概念,函数组合,来控制执行顺序。
这章覆盖了几个函数式编程的风格:数据泛型编程、基本上函数式的编程以及函数响应式编程。 他们之间并没有很大不同,只是在不同场景应用的不同函数式编程模式而已。
上一章粗略提及了一些叫范畴论的东西,下一章我们将学习关于它的更多内容以及如何使用它。
下一章 范畴轮
原标题:JS函数式编程【译】4.4 函数式响应式编程
关键词:JS