本周精读内容是 《逃离 async/await 地狱》

1 引言

终于,async/await 也被吐槽了。Aditya Agarwal 认为 async/await 语法让我们陷入了新的麻烦之中。

其实,笔者也早就觉得哪儿不对劲了,终于有个人把实话说了出来,async/await 可能会带来麻烦。

2 概述

下面是随处可见的现代化前端代码:

(async () => {
  const pizzaData = await getPizzaData(); // async call
  const drinkData = await getDrinkData(); // async call
  const chosenPizza = choosePizza(); // sync call
  const chosenDrink = chooseDrink(); // sync call
  await addPizzaToCart(chosenPizza); // async call
  await addDrinkToCart(chosenDrink); // async call
  orderItems(); // async call
})();

await 语法本身没有问题,有时候可能是使用者用错了。当 pizzaDatadrinkData 之间没有依赖时,顺序的 await 会最多让执行时间增加一倍的 getPizzaData 函数时间,因为 getPizzaDatagetDrinkData 应该并行执行。

回到我们吐槽的回调地狱,虽然代码比较丑,带起码两行回调代码并不会带来阻塞。

看来语法的简化,带来了性能问题,而且直接影响到用户体验,是不是值得我们反思一下?

正确的做法应该是先同时执行函数,再 await 返回值,这样可以并行执行异步函数:

(async () => {
  const pizzaPromise = selectPizza();
  const drinkPromise = selectDrink();
  await pizzaPromise;
  await drinkPromise;
  orderItems(); // async call
})();

或者使用 Promise.all 可以让代码更可读:

(async () => {
  Promise.all([selectPizza(), selectDrink()]).then(orderItems); // async call
})();

看来不要随意的 await,它很可能让你代码性能降低。

3 精读

仔细思考为什么 async/await 会被滥用,笔者认为是它的功能比较反直觉导致的。

首先 async/await 真的是语法糖,功能也仅是让代码写的舒服一些。先不看它的语法或者特性,仅从语法糖三个字,就能看出它一定是局限了某些能力。

举个例子,我们利用 html 标签封装了一个组件,带来了便利性的同时,其功能一定是 html 的子集。又比如,某个轮子哥觉得某个组件 api 太复杂,于是基于它封装了一个语法糖,我们多半可以认为这个便捷性是牺牲了部分功能换来的。

功能完整度与使用便利度一直是相互博弈的,很多框架思想的不同开源版本,几乎都是把功能完整度与便利度按照不同比例混合的结果。

那么回到 async/await 它的解决的问题是回调地狱带来的灾难:

a(() => {
  b(() => {
    c();
  });
});

为了减少嵌套结构太多对大脑造成的冲击,async/await 决定这么写:

await a();
await b();
await c();

虽然层级上一致了,但逻辑上还是嵌套关系,这不是另一个程度上增加了大脑负担吗?而且这个转换还是隐形的,所以许多时候,我们倾向于忽略它,所以造成了语法糖的滥用。

理解语法糖

虽然要正确理解 async/await 的真实效果比较反人类,但为了清爽的代码结构,以及防止写出低性能的代码,还是挺有必要认真理解 async/await 带来的改变。

首先 async/await 只能实现一部分回调支持的功能,也就是仅能方便应对层层嵌套的场景。其他场景,就要动一些脑子了。

比如两对回调:

a(() => {
  b();
});

c(() => {
  d();
});

如果写成下面的方式,虽然一定能保证功能一致,但变成了最低效的执行方式:

await a();
await b();
await c();
await d();

因为翻译成回调,就变成了:

a(() => {
  b(() => {
    c(() => {
      d();
    });
  });
});

然而我们发现,原始代码中,函数 c 可以与 a 同时执行,但 async/await 语法会让我们倾向于在 b 执行完后,再执行 c

所以当我们意识到这一点,可以优化一下性能:

const resA = a();
const resC = c();

await resA;
b();
await resC;
d();

但其实这个逻辑也无法达到回调的效果,虽然 ac 同时执行了,但 d 原本只要等待 c 执行完,现在如果 a 执行时间比 c 长,就变成了:

a(() => {
  d();
});

看来只有完全隔离成两个函数:

(async () => {
  await a();
  b();
})();

(async () => {
  await c();
  d();
})();

或者利用 Promise.all:

async function ab() {
  await a();
  b();
}

async function cd() {
  await c();
  d();
}

Promise.all([ab(), cd()]);

这就是我想表达的可怕之处。回调方式这么简单的过程式代码,换成 async/await 居然写完还要反思一下,再反推着去优化性能,这简直比回调地狱还要可怕。

而且大部分场景代码是非常复杂的,同步与 await 混杂在一起,想捋清楚其中的脉络,并正确优化性能往往是很困难的。但是我们为什么要自己挖坑再填坑呢?很多时候还会导致忘了填。

原文作者给出了 Promise.all 的方式简化逻辑,但笔者认为,不要一昧追求 async/await 语法,在必要情况下适当使用回调,是可以增加代码可读性的。

4 总结

async/await 回调地狱提醒着我们,不要过渡依赖新特性,否则可能带来的代码执行效率的下降,进而影响到用户体验。同时,笔者认为,也不要过渡利用新特性修复新特性带来的问题,这样反而导致代码可读性下降。

当我翻开 redux 刚火起来那段时期的老代码,看到了许多过渡抽象、为了用而用的代码,硬是把两行代码能写完的逻辑,拆到了 3 个文件,分散在 6 行不同位置,我只好用字符串搜索的方式查找线索,最后发现这个抽象代码整个项目仅用了一次。

写出这种代码的可能性只有一个,就是在精神麻木的情况下,一口气喝完了 redux 提供的全部鸡汤。

就像 async/await 地狱一样,看到这种 redux 代码,我觉得远不如所谓没跟上时代的老前端写出的 jquery 代码。

决定代码质量的是思维,而非框架或语法,async/await 虽好,但也要适度哦。

5 更多讨论

讨论地址是:精读《逃离 async/await 地狱》 · Issue #82 · dt-fe/weekly

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。

如果觉得我的文章对你有用,请随意赞赏

14 条评论

看到新来的程序员 满屏的async/await 感到心累

+5 回复

SzHeJason · 5月9日

这不是 async/await 的问题,这是程序员的问题 现在写代码有 async/await 还不用就真的太浪费了
看到 原谅我一生不羁放歌搞文艺 还有那么多人赞 我也是醉了

+2 回复

Jenny · 5月7日

手动点赞:-D
之前就分享过几篇子毅大大的文章,现在终于在SF上发现了踪迹~

+1 回复

幻有夢現 · 5月7日

不是async/await不好,是滥用的情况太多

+1 回复

舒克 · 5月7日

新生语法糖介入时,都有可能变成楼主说的这样。。。

+1 回复

fantix · 5月7日
回调方式这么简单的过程式代码,换成 async/await 居然写完还要反思一下,再反推着去优化性能,这简直比回调地狱还要可怕。

不赞同这里的思路:1、应以顺序执行方式来自然解读 await,类比多线程;2、应时刻注意协作式的并发原理。因此讲清即可,无需 FUD。

赞同不应在不能完全掌控的情况下滥用。

+1 回复

1

赞同

黄子毅 作者 · 5月7日
ubbcou · 5月7日

这里有一个初次大量使用await/async的活生生的例子,press/click事件中由于这个语法糖,事件响应极其缓慢,正在想方设法改变这一现状。

+1 回复

yuexing0921 · 5月7日
写出这种代码的可能性只有一个,就是在精神麻木的情况下,一口气喝完了 redux 提供的全部鸡汤。
对这句话深深的赞同,麻蛋上周为了修改一个简单的代码,然后修改了6个文件,修改后发现有严重的性能问题,调查了半天也搞不清楚,算了直接在需要修改的代码引用一个组件进来,立马就好了,然后代码回滚,修改一个文件就好了。

+1 回复

h_Davy · 5月9日

(async () => {
// results : [pizzaResult, drinkResult]
let results = await Promise.all([selectPizza(), selectDrink()]);
orderItems();
})();

+1 回复

乐行者 · 5月10日

理解其设计初衷而适当使用是需要一定的经验积累,就像function和()=>,为了减少代码的单词而增大阅读难度,降低代码的优雅。存在都是有一定的意义

回复

熊丸子 · 5月11日

使用async/await最好的场景就是在seleniumjs编程中模拟用户操作事件

回复

ZHANGXIANGLIANG · 6 天前

食用指南:需要先阅读原文,不然上下文缺失会觉得 好像盐加得有点少。
食用感受:写得很棒!

回复

小螺号 · 4 天前

按需使用而不是滥用就可以了。

回复

载入中...
Planets
黄子毅 黄子毅

249 声望

发布于专栏

前端精读专栏

精读前端业界好文,每周更新

19 人关注