为什么说 async/await 是一把双刃剑?你用对了么?

2023-01-11 0 328

他们好,很开心又碰面了,我是”web 后端撷取”,由我带着他们一同高度关注后端最前沿、深入细致后端下层控制技术,他们一同不断进步,也热烈欢迎他们高度关注、点赞、珍藏、转贴!

为什么说 async/await 是一把双刃剑?你用对了么?

async/await是两把长弓

1 结语

总算,async/await 也被聊著了。Aditya Agarwal 指出 async/await 句法让他们陷于了捷伊麻烦事当中。

只不过,本栏也早已真的哪里不好意思了,总算有对个人把这话说了出,async/await 可能会增添麻烦事。

2 简述

上面是经常出现的现代后端标识符:

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

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

回到他们聊著的回调地狱,虽然标识符比较丑,带起码两行回调标识符并不会增添阻塞。

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

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

(async () => { const pizzaPromise = selectPizza(); const drinkPromise = selectDrink(); awaitpizzaPromise;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(); constresC = c();await resA; b(); await resC; d();

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

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

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

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

或者利用 Promise.all:

async function ab() { awaita(); 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 虽好,但也要适度哦。

PS: 经过讨论,本栏把原文 async/await 地狱标题改成了 async/await 是把长弓。因为 async/await 并没有回调地狱那么可怕,称它为地狱有误导的可能性。

参考资料

原文链接

https://github.com/ascoders/weekly/blob/master/%E5%89%8D%E6%B2%BF%E6%8A%80%E6%9C%AF/55.%E7%B2%BE%E8%AF%BB%E3%80%8Aasync%20await%20%E6%98%AF%E6%8A%8A%E5%8F%8C%E5%88%83%E5%89%91%E3%80%8B.md

https://www.freecodecamp.org/news/avoiding-the-async-await-hell-c77a0fb71c4c

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务