本周精读内容是 《逃离 async/await 地狱》。html
终于,async/await 也被吐槽了。Aditya Agarwal 认为 async/await 语法让咱们陷入了新的麻烦之中。前端
其实,笔者也早就以为哪儿不对劲了,终于有我的把实话说了出来,async/await 可能会带来麻烦。jquery
下面是随处可见的现代化前端代码:git
(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 语法自己没有问题,有时候多是使用者用错了。当 pizzaData
与 drinkData
之间没有依赖时,顺序的 await 会最多让执行时间增长一倍的 getPizzaData
函数时间,由于 getPizzaData
与 getDrinkData
应该并行执行。github
回到咱们吐槽的回调地狱,虽然代码比较丑,带起码两行回调代码并不会带来阻塞。typescript
看来语法的简化,带来了性能问题,并且直接影响到用户体验,是否是值得咱们反思一下?redux
正确的作法应该是先同时执行函数,再 await 返回值,这样能够并行执行异步函数:api
(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,它极可能让你代码性能下降。异步
仔细思考为何 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();
复制代码
但其实这个逻辑也没法达到回调的效果,虽然 a
与 c
同时执行了,但 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 语法,在必要状况下适当使用回调,是能够增长代码可读性的。
async/await 回调地狱提醒着咱们,不要过渡依赖新特性,不然可能带来的代码执行效率的降低,进而影响到用户体验。同时,笔者认为,也不要过渡利用新特性修复新特性带来的问题,这样反而致使代码可读性降低。
当我翻开 redux 刚火起来那段时期的老代码,看到了许多过渡抽象、为了用而用的代码,硬是把两行代码能写完的逻辑,拆到了 3 个文件,分散在 6 行不一样位置,我只好用字符串搜索的方式查找线索,最后发现这个抽象代码整个项目仅用了一次。
写出这种代码的可能性只有一个,就是在精神麻木的状况下,一口气喝完了 redux 提供的所有鸡汤。
就像 async/await 地狱同样,看到这种 redux 代码,我以为远不如所谓没跟上时代的老前端写出的 jquery 代码。
决定代码质量的是思惟,而非框架或语法,async/await 虽好,但也要适度哦。
若是你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。