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的文档中可以找到对应的说明:MDN | Operator precedence
可以看到yield
的权重非常低,仅高于return
,所以从字面上看,这个执行的结果很符合我们想要的。
而await
关键字的权重要高很多,甚至高于最普通的四则运算,所以必然也是高于三元运算符的。
也就是说await
版本的实际执行是这样子的:
(await true) ? Promise.resolve(123) : Promise.resolve(233) // Promise<123>
那么我们想要获取预期的结果,就需要添加()
来告知解释器我们想要的执行顺序了:
await (true ? Promise.resolve(123) : Promise.resolve(233)) // 123
一定不要漏写 await 关键字
这个其实算不上升级时的坑,在使用co
时也会遇到,但是这是一个很严重,而且很容易出现的问题。
如果有一个异步的操作用来返回一个布尔值,告诉我们他是否为管理员,我们可能会写这样的代码:
async function isAdmin (id) {
if (id === 123) return true
return false
}
if (await isAdmin(1)) {
// 管理员的操作
} else {
// 普通用户的操作
}
因为这种写法接近同步代码,所以遗漏关键字是很有可能出现的:
if (isAdmin(1)) {
// 管理员的操作
} else {
// 普通用户的操作
}
因为async
函数的调用会返回一个Promise
实例,得益于我强大的弱类型脚本语言,Promise
实例是一个Object
,那么就不为空,也就是说会转换为true
,那么所有调用的情况都会进入if
块。
那么解决这样的问题,有一个比较稳妥的方式,强制判断类型,而不是简单的使用if else
,使用类似(a === 1)
、(a === true)
这样的操作。eslint、ts 之类的都很难解决这个问题
一些建议
何时应该用 async ,何时应该直接用 Promise
首先,async
函数的执行返回值就是一个Promise
,所以可以简单地理解为async
是一个基于Promise
的包装:
function fetchData () {
return Promise().resolve(123)
}
// ==>
async function fetchData () {
return 123
}
所以可以认为说await
后边是一个Promise
的实例。
而针对一些非Promise
实例则没有什么影响,直接返回数据。
在针对一些老旧的callback
函数,当前版本的Node
已经提供了官方的转换工具util.promisify,用来将符合Error-first callback
规则的异步操作转换为Promise
实例:
而一些没有遵守这样规则的,或者我们要自定义一些行为的,那么我们会尝试手动实现这样的封装。
在这种情况下一般会采用直接使用Promise
,因为这样我们可以很方便的控制何时应该reject
,何时应该resolve
。
但是如果遇到了在回调执行的过程中需要发起其他异步请求,难道就因为这个Promise
导致我们在内部也要使用.then
来处理么?
function getList () {
return new Promise((resolve, reject) => {
oldMethod((err, data) => {
fetch(data.url).then(res => res.json()).then(data => {
resolve(data)
})
})
})
}
await getList()
但上边的代码也太丑了,所以关于上述问题,肯定是有更清晰的写法的,不要限制自己的思维。
async
也是一个普通函数,完全可以放在任何函数执行的地方。
所以关于上述的逻辑可以进行这样的修改:
function getList () {
return new Promise((resolve, reject) => {
oldMethod(async (err, data) => {
const res = await fetch(data.url)
const data = await res.json()
resolve(data)
})
})
}
await getList()
这完全是一个可行的方案,对于oldMethod
来说,我按照约定调用了传入的回调函数,而对于async
匿名函数来说,也正确的执行了自己的逻辑,并在其内部触发了外层的resolve
,实现了完整的流程。
代码变得清晰很多,逻辑没有任何修改。
合理的减少 await 关键字
await
只能在async
函数中使用,await
后边可以跟一个Promise
实例,这个是大家都知道的。
但是同样的,有些await
其实并没有存在的必要。
首先有一个我面试时候经常会问的题目:
Promise.resolve(Promise.resolve(123)).then(console.log) // ?
最终输出的结果是什么。
这就要说到resolve
的执行方式了,如果传入的是一个Promise
实例,亦或者是一个thenable
对象(简单的理解为支持.then((resolve, reject) => {})
调用的对象),那么resolve
实际返回的结果是内部执行的结果。
也就是说上述示例代码直接输出123
,哪怕再多嵌套几层都是一样的结果。
通过上边所说的,不知大家是否理解了 合理的减少 await 关键字 这句话的意思。
结合着前边提到的在async
函数中返回数据是一个类似Promise.resolve
/Promise.reject
的过程。
而await
就是类似监听then
的动作。
所以像类似这样的代码完全可以避免:
const imgList = []
async function getImage (url) {
const res = await fetch(url)
return await res.blob()
}
await Promise.all(imgList.map(async url => await getImage(url)))
// ==>
async function getImage (url) {
const res = fetch(url)
return res.blob()
}
await Promise.all(imgList.map(url => getImage(url)))
上下两种方案效果完全相同。
Express 与 koa 的升级
首先,Express
是通过调用response.send
来完成请求返回数据的。
所以直接使用async
关键字替换原有的普通回调函数即可。
而Koa
也并不是说你必须要升级到2.x
才能够使用async
函数。
在Koa1.x
中推荐的是generator
函数,也就意味着其内部是调用了co
来帮忙做转换的。
而看过co
源码的小伙伴一定知道,里边同时存在对于Promise
的处理。
也就是说传入一个async
函数完全是没有问题的。
但是1.x
的请求上下文使用的是this
,而2.x
则是使用的第一个参数context
。
所以在升级中这里可能是唯一需要注意的地方,在1.x
不要使用箭头函数来注册中间件。
// express
express.get('/', async (req, res) => {
res.send({
code: 200
})
})
// koa1.x
router.get('/', async function (next) {
this.body = {
code: 200
}
})
// koa2.x
router.get('/', async (ctx, next) => {
ctx.body = {
code: 200
}
})
小结
重构项目是一件很有意思的事儿,但是对于一些注释文档都很缺失的项目来说,重构则是一件痛苦的事情,因为你需要从代码中获取逻辑,而作为动态脚本语言的JavaScript
,其在大型项目中的可维护性并不是很高。
所以如果条件允许,还是建议选择TypeScript
之类的工具来帮助更好的进行开发。
async语法升级踩坑小记的更多相关文章
- jQuery升级踩坑大全
jQuery升级踩坑大全 背景 jQuery想必各个web工程师都再熟悉不过了,不过现如今很多网站还采用了很古老的jQuery版本.其实如果早期版本使用不当,可能会有DOMXSS漏洞,非常建议升级到j ...
- Ubuntu 16.04 安装Mysql 5.7 踩坑小记
title:Ubuntu 16.04 安装Mysql 5.7 踩坑小记 date: 2018.02.03 安装mysql sudo apt-get install mysql-server mysql ...
- MySql 踩坑小记
MySql 踩坑一时爽,一直踩啊一直爽... 以下记录刚踩的三个坑,emmm... 首先是远程机子上创建表错误(踩第一个坑),于是将本地机器 MySql 版本回退至和远程一致(踩第二个坑),最后在 ...
- jQuery升级踩坑之路
1.使用了被废弃的jQuery.browser属性 jQuery 从 1.9 版开始,移除了 $.browser 和 $.browser.version , 取而代之的是 $.support . 在更 ...
- dubbo 2.7应用级服务发现踩坑小记
本文已收录 https://github.com/lkxiaolou/lkxiaolou 欢迎star. 背景 本文记录最近一位读者反馈的dubbo 2.7.x中应用级服务发现的问题,关于dubbo应 ...
- 支付宝使用流程和踩坑小记(附Demo)
# 支付宝使用整理 html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym, ...
- Guava Lists.transform踩坑小记<转>
1.问题提出 1.前段时间在项目中用到Lists.transform返回的List,在对该list修改后发现修改并没有反映在结果里,研究源码后发现问题还挺大.下面通过单步调试的结果来查看Guava L ...
- SpringBoot2.x升级踩坑--新增Configuration property name限制
最近公司项目在做SpringBoot的升级,在升级过程中遇到了一些问题,简单记录一下,做个分享.另外,本文中的程序只为示例代码,并非公司生产环境代码. 遇到什么问题 从SpringBoot1.x升级到 ...
- 代码语法高亮踩坑-原理,问题, PRE元素及htmlentity
语法高亮库基础原理 在研究使用能够在web页面上代码语法高显的解决方案时,发现有很多现成的开源库.比较中意的有prism.js,highlightjs.他们的原理基本上核心就两点: 1. 利用html ...
随机推荐
- 不再混淆,一次搞懂!图解flexbox十余个属性
原文首发于个人博客:不再混淆,一次搞懂!图解flexbox十余个属性 flexbox的发明简直是csser的一大福音,终于可以不再需要为垂直居中一个元素而绞尽脑汁了.同时它还能够实现弹性布局,可以说没 ...
- codeforces 1041 E.Vasya and Good Sequences(暴力?)
E. Vasya and Good Sequences time limit per test 2 seconds memory limit per test 256 megabytes input ...
- Chrome神器Vimium快捷键学习记录
今天下午折腾了一下Chrome下面的一个插件Vimium的使用,顿时发现该插件功能强大,能够满足减少鼠标的使用.至于为何要使用这个插件,源于我手腕上的伤一直没有好,使用鼠标的时候有轻微的疼痛.而且,由 ...
- P3919 【模板】可持久化数组(可持久化线段树/平衡树)
题目描述 如题,你需要维护这样的一个长度为 N 的数组,支持如下几种操作 在某个历史版本上修改某一个位置上的值 访问某个历史版本上的某一位置的值 此外,每进行一次操作(对于操作2,即为生成一个完全一 ...
- 【刷题】BZOJ 1537 [POI2005]Aut- The Bus
Description Byte City 的街道形成了一个标准的棋盘网络 – 他们要么是北南走向要么就是西东走向. 北南走向的路口从 1 到 n编号, 西东走向的路从1 到 m编号. 每个路口用两个 ...
- BZOJ 2243 染色 | 树链剖分模板题进阶版
BZOJ 2243 染色 | 树链剖分模板题进阶版 这道题呢~就是个带区间修改的树链剖分~ 如何区间修改?跟树链剖分的区间询问一个道理,再加上线段树的区间修改就好了. 这道题要注意的是,无论是线段树上 ...
- hdu6057 Kanade's convolution 【FWT】
题目链接 hdu6057 题意 给出序列\(A[0...2^{m} - 1]\)和\(B[0...2^{m} - 1]\),求所有 \[C[k] = \sum\limits_{i \; and \; ...
- Java之JDBC连接池
数据库连接池 连接池的概述 概念:其实就是一个容器(集合),存放数据库连接的容器. 当系统初始化好后,容器被创建,容器中会申请一些连接对象,当用户来访问数据库时, 从容器中获取连接对象,用户访问完之后 ...
- cocoaPods安装、更新第三方库
pod install 换成 pod install --verbose --no-repo-update pod update 换成 pod update --verbose --no-repo-u ...
- 「Vue」Vue cli3中引用mui-ui问题及解决办法
1.引用mui.js无效,top-bar划动,numbox点击无效等问题 解决办法: -main.js中import mui from './lib/mui/js/mui.js' Vue.protot ...