async语法升级踩坑小记

从今年过完年回来,三月份开始,就一直在做重构相关的事情。
就在今天刚刚上线了最新一次的重构代码,希望高峰期安好,接近半年的Node.js代码重构。
包含从callback+async.waterfall到generator+co,统统升级为了async,还顺带推动了TypeScript在我司的使用。
这些日子也踩了不少坑,也总结了一些小小的优化方案,进行精简后将一些比较关键的点,拿出来分享给大家,希望有同样在做重构的小伙伴们可以绕过这些。

为什么要升级

首先还是要谈谈改代码的理由,毕竟重构肯定是要有合理的理由的。
如果单纯想看升级相关事项可以直接选择跳过这部分。

Callback

从最原始的开始说起,期间确实遇到了几个年代久远的项目,Node 0.x,使用的普通callback,也有一些会应用上async.waterfall这样在当年看起来很优秀的工具。

// 普通的回调函数调用 var fs = require('fs') fs.readFile('test1.txt', function (err, data1) { if (err) return console.error(err) fs.readFile('test2.txt', function (err, data2) { if (err) return console.error(err) // 执行后续逻辑 console.log(data1.toString() + data2.toString()) // ... }) }) // 使用了async以后的复杂逻辑 var async = require('fs') async.waterfall([ function (callback) { fs.readFile('test1.txt', function (err, data) { if (err) callback(err) callback(null, data.toString()) }) }, function (result, callback) { fs.readFile('test2.txt', function (err, data) { if (err) callback(err) callback(null, result + data.toString()) }) } ], function (err, result) { if (err) return console.error(err) // 获取到正确的结果 console.log(result) // 输出两个文件拼接后的内容 })

虽说async.waterfall解决了callback hell的问题,不会出现一个函数前边有二三十个空格的缩进。
但是这样的流程控制在某些情况下会让代码变得很诡异,例如我很难在某个函数中选择下一个应该执行的函数,而是只能按照顺序执行,如果想要进行跳过,可能就要在中途的函数中进行额外处理:

async.waterfall([ function (callback) { if (XXX) { callback(null, null, null, true) } else { callback(null, data1, data2) } }, function (data1, data2, isPass, callback) { if (isPass) { callback(null, null, null, isPass) } else { callback(null, data1 + data2) } } ])

所以很可能你的代码会变成这样,里边存在大量的不可读的函数调用,那满屏充斥的null占位符。

所以callback这种形式的,一定要进行修改, 这属于难以维护的代码

Generator

实际上generator是依托于co以及类似的工具来实现的将其转换为Promise,从编辑器中看,这样的代码可读性已经没有什么问题了,但是问题在于他始终是需要额外引入co来帮忙实现的,generator本身并不具备帮你执行异步代码的功能。
不要再说什么async/await是generator的语法糖了

因为我司Node版本已经统一升级到了8.11.x,所以async/await语法已经可用。
这就像如果document.querySelectorAll、fetch已经可以满足需求了,为什么还要引入jQuery呢。

所以,将generator函数改造为async/await函数也是势在必行。

期间遇到的坑

将callback的升级为async/await其实并没有什么坑,反倒是在generator + co 那里遇到了一些问题:

数组执行的问题

在co的代码中,大家应该都见到过这样的:

const results = yield list.map(function * (item) { return yield getData(item) })

在循环中发起一些异步请求,有些人会告诉你,从yield改为async/await仅仅替换关键字就好了。

那么恭喜你得到的results实际上是一个由Promise实例组成的数组。

const results = await list.map(async item => { return await getData(item) }) console.log(results) // [Promise, Promise, Promise, ...]

因为async并不会判断你后边的是不是一个数组(这个是在co中有额外的处理)而仅仅检查表达式是否为一个Promise实例。
所以正确的做法是,添加一层Promise.all,或者说等新的语法await*,Node.js 10.x貌似还不支持。。

// 关于这段代码的优化方案在下边的建议中有提到 const results = await Promise.all(list.map(async item => { return await getData(item) })) console.log(results) // [1, 2, 3, ...] await / yield 执行顺序的差异

这个一般来说遇到的概率不大,但是如果真的遇到了而栽了进去就欲哭无泪了。

首先这样的代码在执行上是没有什么区别的:

yield 123 // 123 await 123 // 123

这样的代码也是没有什么区别的:

yield Promise.resolve(123) // 123 await Promise.resolve(123) // 123

但是这样的代码,问题就来了:

yield true ? Promise.resolve(123) : Promise.resolve(233) // 123 await true ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>

从字面上我们其实是想要得到yield那样的效果,结果却得到了一个Promise实例。
这个是因为yield、await两个关键字执行顺序不同所导致的。

在MDN的文档中可以找到对应的说明:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsppdx.html