阅读 1120

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

关于译者:这是一个流淌着沪江血液的纯粹工程:认真,是 HTML 最坚实的梁柱;分享,是 CSS 里最闪耀的一瞥;总结,是 JavaScript 中最严谨的逻辑。经过捶打磨练,成就了本书的中文版。本书包含了函数式编程之精髓,希望可以帮助大家在学习函数式编程的道路上走的更顺畅。比心。

译者团队(排名不分先后):阿希bluekenbrucechamcfanlifedailkyoko-dfl3velilinsLittlePineappleMatildaJin冬青pobusamaCherry萝卜vavd317vivaxy萌萌zhouyao

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

阅读到这里,你已经学习了我所说的所有轻量级函数式编程的基础概念,在本章节中,我们将把这些概念应有到不同的情景当中,但绝对不会有新的知识点。

到目前为止,我们所说的一切都是同步的,意味着我们调用函数,传入参数后马上就会得到返回值。大部分的情况下是没问题的,但这几乎满足不了现有的 JS 应用。为了能在当前的 JS 环境里使用上函数式编程,我们需要去了解异步的函数式编程。

本章的目的是拓展我们对用函数式编程管理数据的思维,以便之后我们在更多的业务上应用。

时间状态

在你所有的应用里,最复杂的状态就是时间。当你操作的数据状态改变过程比较直观的时候,是很容易管理的。但是,如果状态随着时间因为响应事件而隐晦的变化,管理这些状态的难度将会成几何级增长。

我们在本文中介绍的函数式编程可以让代码变得更可读,从而增强了可靠性和可预见性。但是当你添加异步操作到你的项目里的时候,这些优势将会大打折扣。

必须明确的一点是:并不是说一些操作不能用同步来完成,或者触发异步行为很容易。协调那些可能会改变应用程序的状态的响应,这需要大量额外的工作。

所以,作为作者的你最好付出一些努力,或者只是留给阅读你代码的人一个难题,去弄清楚如果 A 在 B 之前完成,项目中状态是什么,还有相反的情况是什么?这是一个浮夸的问题,但以我的观点来看,这有一个确切的答案:如果可以把复杂的代码变得更容易理解,作者就必须花费更多心思。

减少时间状态

异步编程最为重要的一点是通过抽象时间来简化状态变化的管理。

为说明这一点,让我们先来看下一种有竞争状态(又称,时间复杂度)的糟糕情况,且必须手动去管理里面的状态:

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(..) 之后运行,但我们不能这么做,因为我们需要让 2 个查询同时执行。

所以,为了让这个基于时间的复杂状态正常化,我们用相应的 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;
	} );
} );
复制代码

现在 onOrders(..) 回调函数存在 onCustomer(..) 回调函数里,所以他们各自的执行顺序是可以保证的。在各自的 then(..) 运行之前 lookupCustomer(..)lookupOrders(..) 被分别的调用,两个查询就已经并行的执行完了。

这可能不太明显,但是这个代码里还有其他内在的竞争状态,那就是 promise 的定义没有被体现出来。如果 orders 的查询在把 onOrders(..) 回调函数被 ordersPromise.then(..) 调用前完成,那么就需要一些比较智能的 东西 来保存 orders 直到 onOrders(..) 能被调用。 同理,record (或者说customer)对象是否能在 onCustomer(..) 执行时被接收到。

这里的 东西 和我们之前讨论过的时间复杂度类似。但我们不必去担心这些复杂性,无论是编码或者是读(更为重要)这些代码的时候,因为对我们来说,promise 所处理的就是时间复杂度上的问题。

promise 以时间无关的方式来作为一个单一的值。此外,获取 promise 的返回值是异步的,但却是通过同步的方法来赋值。或者说, promise 给 = 操作符扩展随时间动态赋值的功能,通过可靠的(时间无关)方式。

接下来我们将探索如何以相同的方式,在时间上异步地拓展本书之前同步的函数式编程操作。

积极的 vs 惰性的

积极的和惰性的在计算机科学的领域并不是表扬或者批评的意思,而是描述一个操作是否立即执行或者是延时执行。

我们在本例子中看到的函数式编程操作可以被称为积极的,因为它们同步(即时)地操作着离散的即时值或值的列表/结构上的值。

回忆下:

var a = [1,2,3]

var b = a.map( v => v * 2 );

b;			// [2,4,6]
复制代码

这里 ab 的映射就是积极的,因为它在执行的那一刻映射了数组 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 里。

注意: mapLazy(..) 的实现没有被写出来,是因为它是虚构的方法,是不存在的。如果要实现 ab 之间的惰性的操作,那么简单的数组就需要变得更加聪明。

考虑下把 ab 关联到一起的好处,无论何时何地,你添加一个值进 a 里,它都将改变且映射到 b 里。它比同为声明式函数式编程的 map(..) 更强大,但现在它可以随时地变化,进行映射时你不用知道 a 里面所有的值。

** 【上一章】翻译连载 | 第 9 章:递归(下)-《JavaScript轻量级函数式编程》 |《你不知道的JS》姊妹篇 **

iKcamp原创新书《移动Web前端高效开发实战》已在亚马逊、京东、当当开售。

iKcamp官网:www.ikcamp.com


2019年,iKcamp原创新书《Koa与Node.js开发实战》已在京东、天猫、亚马逊、当当开售啦!

关注下面的标签,发现更多相似文章
评论