如果要整体了解一个人的核心 JavaScript 技能,我最感兴趣的是他们会如何使用闭包以及如何充分利用异步。—— Jake Archibald
我们前篇谈了很多关于【闭包】的理解了,所以你应该会知道,我们现在将要谈的就是 ——【异步】。
- 点赞富三代,关注美一生!
再看异步
我们为什么觉得“异步问题”复杂呢?
其中很重要的一个原因是 —— 时间!时间将我们对数据的操作、管理,变复杂了好几个量级!
(需要特别提出并明确的是:异步和同步之间是可以相互转化的! 我们使用异步或者同步取决于 —— 如何使代码更加可读!)
函数式编程给出了实现“代码更可读”的落地原则(已多次回顾):
- 严格控制显示的输入输出;
- 封装高级函数,比如偏函数、柯里化实现参数的时域分离;
- 封装高级函数,比如函数组装,形成黑盒;
- 对其它基础方法进行封装,比如数组操作;
- ......
所以我们可以期待,异步在函数式编程中的表现!
减少时间状态
上代码:
var customerId = 42;
var customer;
lookupCustomer( customerId, function onCustomer(customerRecord){ // 通过查询用户来查询订单
var orders = customer ? customer.orders : null;
customer = customerRecord;
if (orders) {
customer.orders = orders;
}
} );
lookupOrders( customerId, function onOrders(customerOrders){ // 直接查询订单
if (!customer) {
customer = {};
}
customer.orders = customerOrders;
} );
onCustomer(..) 和 onOrders(..) 是两个【回调函数】释义,两者执行的先后顺序并不能确定,所以它是一个基于时间的复杂状态。
释义:回调函数其实就是一个参数,将这个函数作为参数传到另一个函数里面,当那个函数执行完之后,再执行传进去的这个函数。
- 怎样去确定它们在时间上执行的先后关系呢?
通常来说,我们最先想到的是:把 lookupOrders(..) 写到 onCustomer(..) 里面,那我们就可以确认 onOrders(..) 会在 onCustomer(..) 之后运行。
这样写,对吗?
不对!因为 onCustomer(..) 、onOrders(..) 这两个回调函数的关系更像是一种竞争关系(都是赋值 customer.orders),它们应该并行执行,而不是串行执行。
即:我不管你们谁先执行,谁先执行完,谁就赋值给 customer.orders!
那我们的思路应该是:
用相应的 if-声明在各自的回调函数里来检查外部作用域的变量 customer。当各自的回调函数被执行,将会去检测 customer 的状态,从而确定各自的执行顺序,如果 customer 在回调函数里还没被定义,那他就是先运行的,否则则是第二个运行的。
不过,这样让代码又变得更加难阅读!!函数内部赋值依赖于外部变量、甚至受外部回调函数的影响。
那究竟怎么办呢?
最终,我们借用 JS promise 减少这个时间状态,将异步转成同步:
var customerId = 42;
var customerPromise = lookupCustomer( customerId );
var ordersPromise = lookupOrders( customerId );
customerPromise.then( function onCustomer(customer){
ordersPromise.then( function onOrders(orders){
customer.orders = orders;
} );
} );
两个 .then(..) 运行之前,lookupCustomer(..) 和 lookupOrders(..) 已被同步调用,满足并行执行,谁先结束,谁赋值给 customer.orders,所以我们不需要知道谁先谁后!
在这样的实现下,不再需要时间先后的概念!减少了时间状态!!代码的可读性更高了!!
惰性的数组
var a = [1,2,3]
var b = a.map( v => v * 2 );
b; // [2,4,6]
这是一个积极的数组,因为它们同步(即时)地操作着离散的即时值或值的列表/结构上的值。
什么意思?
a 映射到 b,再去修改 a ,b 不会收到影响。
var a = [];
var b = mapLazy( a, v => v * 2 );
a.push( 1 );
a[0]; // 1
b[0]; // 2
a.push( 2 );
a[1]; // 2
b[1]; // 4
而这,是一个惰性的数组,mapLazy(..) 本质上 “监听” 了数组 a,只要一个新的值添加到数组的末端(push(..)),它都会运行映射函数 v => v * 2 并把改变后的值添加到数组 b 里。
什么意思?
a 映射到 b,再去修改 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 的过程是异步的!
b ,是 map 映射后的数组,但更重要的是,b 是反应性的,我们对 b 加了一个类似监听器的东西。
我们称前半部分为发布者,后半部分为订阅者。
你一定会疑问:定义这个懒惰的数组,有何作用?这里发布者、订阅者,又是几个意思?
这里直接给出解答:
正如 promise 从单个异步操作中抽离出我们所担心的时间状态,发布订阅模式也能从一系列的值或操作中抽离(分割)时间状态;
我们分离 【发布者】 和 【订阅者】 的相关代码,让代码应该各司其职。这样的代码组织可以很大程度上提高代码的可读性和维护性。
这里再多小结一句:时间让异步更加复杂,函数式编程在异步下的运用就是减少或直接干掉时间状态。
想象下 a 还可以被绑定上一些其他的事件上,比如说用户的鼠标点击事件和键盘按键事件,服务端来的 websocket 消息等。
在这些情况下,a 没必要关注自己的时间状态。
// 发布者:
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 );
}
};
这里,【时间】 与 【a、b】 之间的关系是声明式的,不是命令式的。
我们进一步,把 b = a.map(..) 替换成 b.onValue(v),尽量避免将 b 的逻辑夹杂在 a 中,让关注点更加分离!
上述的 LazyArray 又可叫做 observable!(当然,它不止用在 map 方法中)
现在已经有各种各样的 Observables 的库类,最出名的是 RxJS 和 Most。
以 RxJS 为例:
// 发布者:
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 还定义了超过 100 个可以在有新值添加时才触发的方法。就像数组一样。每个 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 );
} );
- 通常,subscribe(..) 方法都会在链式写法的最后被调用
阶段小结
本篇介绍了【异步】在函数式编程中的表现。
原则是:对于那些异步中有时态的操作,基础的函数式编程原理就是将它们变为无时态的应用。即减少时间状态!
就像 promise 创建了一个单一的未来值,我们可以创建一个积极的列表的值来代替像惰性的observable(事件)流的值。
我们介绍了 RxJS 库,后续我们还会介绍更多优美的 JS 函数式编程库!
(俗话说的好,三方库选的好,下班都很早!!)
现在本瓜有点明白那句话了:看一门语言是不是函数式编程,取决于它的核心库是不是函数式编程。
也许我们还不熟悉像 RxJS 这类库,但我们慢慢就会越来越重视它们,越来越使用它们,越来越领会到它们!!
异步,以上。
我是掘金安东尼: 一名人气前端技术博主(文章 100w+ 阅读量)
终身写作者(INFP 写作人格)
坚持与热爱(简书打卡 1000 日)
我能陪你一起度过漫长技术岁月吗(以梦为马)
觉得不错,给个点赞和关注吧(这是我最大的动力 )b( ̄▽ ̄)d