async语法升级踩坑小记

从今年过完年回来,三月份开始,就一直在做重构相关的事情。
就在今天刚刚上线了最新一次的重构代码,希望高峰期安好,接近半年的Node.js代码重构。
包含从callback+async.waterfallgenerator+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.querySelectorAllfetch已经可以满足需求了,为什么还要引入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实例。
这个是因为yieldawait两个关键字执行顺序不同所导致的。

在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之类的工具来帮助更好的进行开发。

原文地址:https://www.cnblogs.com/jiasm/p/9716722.html

时间: 2024-10-07 23:56:20

async语法升级踩坑小记的相关文章

jQuery升级踩坑大全

背景 jQuery想必各个web工程师都再熟悉不过了,不过现如今很多网站还采用了很古老的jQuery版本.其实如果早期版本使用不当,可能会有DOMXSS漏洞,非常建议升级到jQuery 1.9.x或以上版本.前段时间我就主导了这件事情,把公司里我们组负责的项目jQuery版本从1.4.2升级到了jQuery 1.11.3.jQuery官方也为类似升级工作提供了jQuery Migrate插件. 言归正传. 坑从何处来 jQuery 1.11.3是1.x时代的最后一个版本(作者更新:2016年1月

kafka与zookeeper管理之kafka-manager踩坑小记

在elk集群搭建过程中,为了极大程度的利用服务器资源,kafka.zookeeper.logstash规划混跑在了同一组服务器上.随着业务量的增加,要频繁增加调整kafka的topic,出现问题时还要去服务器敲命令查看kafka和zookeeper的相关信息,效率低而且不方便,于是就考虑到用kafka的管理工具kafka-manager,安装配好后,整个集群的状态一目了然,而且可以方便的进行topic的操作.消费情况的查询.broker各种状态指标的查询等,非常方便,各种信息一目了然,安装配置过

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-client 测试是否安装成功 sudo netstat -tap | grep mysql 相关操作 登录 mysql -uroot -p 检查MySQL服务器占用端口 netstat -nlt|grep 3306 检查MySQL服务器系统进程 ps -aux|grep mysql 查看数据库的

jQuery升级踩坑之路

1.使用了被废弃的jQuery.browser属性 jQuery 从 1.9 版开始,移除了 $.browser 和 $.browser.version , 取而代之的是 $.support . 在更新的 2.0 版本中,将不再支持 IE 6/7/8. 以后,如果用户需要支持 IE 6/7/8,只能使用 jQuery 1.9. 如果要全面支持 IE,并混合使用 jQuery 1.9 和 2.0, 官方的解决方案是: <!--[if lt IE 9]> <script src='jquer

算法踩坑小记

经过前面研究图像算法和近阶段研究视频和音频算法的经历经验. 在2019年快要来临的时候,写下这篇小记. 目的很简单,总结过往,展望未来. 这里列举一些本人在算法上踩过的坑和出坑思路. 主要是数据标准化问题. 1.临界值问题  (最大值,最小值,阈值,无穷小,无穷大) 最早做一键修图的时候,在这个坑上踩了太多次. 简单描述就是, (示例伪代码例子仅供理解思考参考,不具有实际意义) 1.1 梯度消失 如果一个算法在计算过程中,存在最小值(无穷小,一般为0或接近0的数), 那就很可能出现"梯度消失&q

Guava Lists.transform踩坑小记&lt;转&gt;

1.问题提出 1.前段时间在项目中用到Lists.transform返回的List,在对该list修改后发现修改并没有反映在结果里,研究源码后发现问题还挺大.下面通过单步调试的结果来查看Guava Lists.transform使用过程中需要注意的地方. a.对原有的list列表修改会影响Lists.transform已经生成列表 由上图可以看出,对原数据集personDbs的修改会直接影响到Lists.transform方法返回的结果personVos,这是很危险的,如果在使用的过程中不注意的

webpack升级踩坑

webpack3.8.1 => webpack4.39.2 1.error:webpack is not a function fix scripts/start.js const compiler = createCompiler(webpack, config, appName, urls, useYarn); => const compiler = createCompiler({ webpack, config, appName, urls, useYarn }); 2.error:

HTTP访问控制(CORS)踩坑小记

前几天在帮后端排查一个cors的问题的时候发现的一些小坑特此记录 ** cors的本质是出于安全原因,浏览器限制从脚本内发起的跨源HTTP请求. 例如,XMLHttpRequest和FetchAPI遵循同源策略. 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非使用CORS头文件.跨域并非一定是浏览器限制了跨站请求,也有可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了.最好的例子是 CSRF跨站攻击原理,请求是发送到了后端服务器无论是否跨域!注意:有

Laravel踩坑小记

背景:使用laravel项目的命令行惊现错误php artisan companyCustomerStatisticsCommand In Connection.php line 664: could not find driver (SQL: select max(`customer_id`) as aggregate from `statistic_customer`) 排查:1.浏览器直接访问项目,数据库均能正常访问2.浏览器查看phpinfo(), 获取到php版本信息 PHP Vers