XDM,JS如何函数式编程?看这就够了!(六)

1,966 阅读7分钟

第六篇,我们首先再次重申那句经典的话:

如果要整体了解一个人的核心 JavaScript 技能,我最感兴趣的是他们会如何使用闭包以及如何充分利用异步。—— Jake Archibald

我们前篇谈了很多关于【闭包】的理解了,所以你应该会知道,我们现在将要谈的就是 ——【异步】。

  • 点赞富三代,关注美一生!👍👍👍👍👍👍

IREWpE.md.png

再看异步

我们为什么觉得“异步问题”复杂呢?

其中很重要的一个原因是 —— 时间!时间将我们对数据的操作、管理,变复杂了好几个量级!

(需要特别提出并明确的是:异步和同步之间是可以相互转化的! 我们使用异步或者同步取决于 —— 如何使代码更加可读!)

函数式编程给出了实现“代码更可读”的落地原则(已多次回顾):

  1. 严格控制显示的输入输出;
  2. 封装高级函数,比如偏函数、柯里化实现参数的时域分离;
  3. 封装高级函数,比如函数组装,形成黑盒;
  4. 对其它基础方法进行封装,比如数组操作;
  5. ......

所以我们可以期待,异步在函数式编程中的表现!

减少时间状态

上代码:

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 加了一个类似监听器的东西。

我们称前半部分为发布者,后半部分为订阅者

你一定会疑问:定义这个懒惰的数组,有何作用?这里发布者、订阅者,又是几个意思?

这里直接给出解答:

  1. 正如 promise 从单个异步操作中抽离出我们所担心的时间状态,发布订阅模式也能从一系列的值或操作中抽离(分割)时间状态;

  2. 我们分离 【发布者】 和 【订阅者】 的相关代码,让代码应该各司其职。这样的代码组织可以很大程度上提高代码的可读性和维护性。

这里再多小结一句:时间让异步更加复杂,函数式编程在异步下的运用就是减少或直接干掉时间状态。

想象下 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 的库类,最出名的是 RxJSMost

以 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(..) 方法都会在链式写法的最后被调用

更多关于:RxJS

阶段小结

本篇介绍了【异步】在函数式编程中的表现。

原则是:对于那些异步中有时态的操作,基础的函数式编程原理就是将它们变为无时态的应用。即减少时间状态

就像 promise 创建了一个单一的未来值,我们可以创建一个积极的列表的值来代替像惰性的observable(事件)流的值。

我们介绍了 RxJS 库,后续我们还会介绍更多优美的 JS 函数式编程库!

(俗话说的好,三方库选的好,下班都很早!!)

现在本瓜有点明白那句话了:看一门语言是不是函数式编程,取决于它的核心库是不是函数式编程。

也许我们还不熟悉像 RxJS 这类库,但我们慢慢就会越来越重视它们,越来越使用它们,越来越领会到它们!!

异步,以上。

预告:第七篇(系列完结篇) —— 实践 + 库推荐!

我是掘金安东尼,公众号【掘金安东尼】,输入暴露输出,技术洞见生活!!!