为何说async/await并不仅仅是Promise语法糖?

前有科技后进阶 2024-05-25 07:09:19

大家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。

注意:以下文章内容大部分来自 Zhenghao 发表的《Why Async/Await Is More Than Just Syntactic Sugar》,但是对部分内容进行了调整。

首先给出结论,async/await 不仅仅是 Promise 的语法糖,因为 async/await 确实提供实实在在的诸多好处:

async/await 允许使用同步编程中可用的所有语言结构,从而更具表现力和可读性async/await 统一了异步编程的体验async/await 提供更好的错误捕获信息(Stack Trace)1.JavaScript 异步编程历史

异步编程在 JavaScript 中非常常见,每当需要执行诸如: Web 服务调用、文件访问或数据库操作时,异步就是防止单线程下 UI 被阻塞的最常用方法。

在 JavaScript 在 ES2015(ES6)中进行重大升级之前,回调是开发者处理异步编程的常规方式。 此时,确保异步操作执行顺序的唯一方法是将一个回调嵌套在另一个回调中,从而产生了所谓的回调地狱。

// 多级嵌套fs.readdir(source, function (err, files) { if (err) { console.log('Error finding files:' + err) } else { files.forEach(function (filename, fileIndex) { console.log(filename) gm(source + filename).size(function (err, values) { // 嵌套 }) }) }})

ES2015 引入了 Promise ,其是用于异步操作的一流对象,开发者可以轻松地对其进行传递、组合(Compose)、聚合(Aggregate)和应用转换(Transformation),而执行顺序则可以通过 then 方法链接清晰地表达。

注意:JavaScript 中 Promise 并非首创,其灵感来自一种非常古老的语言,即 E 语言,其创建者 Mark Miller 也是 TC39 的代表,而 async/await 语法是从 C# 借用的。

Promise 作为一个强大的原语,听起来异步编程问题在 JavaScript 中已经彻底解决。然而不幸的是,有时 Promise 的级别可能有点太低,无法使用。

2. 有时 Promise 可能太偏底层而很难使用

尽管出现了 Promise,但 JavaScript 中的异步编程仍然需要更高级的语言结构。比如以下示例,需要一个函数以某个时间间隔轮询 API, 而当达到最大重试次数时,则 resolve 为 null。

以下是 Promise 一种可能的解决方案:

let count = 0;function apiCall() { return new Promise((resolve) => // 第 6 次重试时候 resolve 为字符串'value',否则为 null count++ === 5 ? resolve('value') : resolve(null) );}function sleep(interval) { return new Promise((resolve) => setTimeout(resolve, interval));}function poll(retry, interval) { return new Promise((resolve) => { // 为了简洁起见省略了错误处理 if (retry === 0) resolve(null); apiCall().then((val) => { if (val !== null) resolve(val); else { sleep(interval).then(() => { // 注意:如果 resolve 参数是 Promise 则依赖于参数 Promise 的状态 resolve(poll(retry - 1, interval)); }); } }); });}poll(6, 1000).then(console.log);// 输出值'value'

该方案的直观性和可读性取决于开发者对 Promise 的熟悉程度,以及 Promise.resolve 如何扁平化(flat)Promise 和递归。因此,很多开发者可能更加喜欢使用 setInterval 来实现类似逻辑:

const pollInterval = (retry, interval) => { return new Promise((resolve) => { let intervalToken, timeoutToken; // 每隔一定时间去执行指定 API 函数 intervalToken = setInterval(async () => { const result = await apiCall(); if (result !== null) { // 如果有结果返回则直接 resolve clearInterval(intervalToken); clearTimeout(timeoutToken); resolve(result); } }, interval); // 最大尝试次数后清空定时器同时 resolve 为 null timeoutToken = setTimeout(() => { clearInterval(intervalToken); resolve(null); }, retry * interval); });};3. 使用 async/await 语法重写 Promise 递归和 setInterval

接下来使用 async/await 语法重写上面 Promise 的解决方案:

async function poll(retry, interval) { while (retry>= 0) { const value = await apiCall().catch((e) => {}); // 为了简洁起见省略了错误处理 if (value !== null) return value; await sleep(interval); retry--; } return null;}

大多数开发者都会觉得上述解决方案更具可读性,因为能够使用所有正常的语言结构,例如:while、try-catch 等同步操作。

这可能是 async/await 的最大卖点 ,即:开发者能够以同步方式编写异步代码。 而另一方面,这可能也是对 async/await 最常见的反对意见的来源。

顺便说一句,await 甚至具有正确的运算符优先级,因此 await a + await b 确实意味着 (await a) + (await b),而不是 await (a + await b)。

当然,以上 while 循环的示例还可以通过递归重写:

const pollAsyncAwait = async (retry, interval) => { if (retry < 0) return null; const value = await apiCall().catch((e) => {}); // 错误处理 if (value !== null) return value; await sleep(interval); return pollAsyncAwait(retry - 1, interval);};4.async/await 提供同步和异步代码的统一体验

async/await 的另一个好处是,await 自动将任何非 Promises 的内容(non-thenables)包装到 Promises 中。 此时,await 的语义等同于 Promise.resolve,这也意味着开发者可以 await 任何东西:

function fetchValue() { return 1;}async function fn() { const val = await fetchValue(); console.log(val); // 1}// 以上代码和下面完全一样function fn() { Promise.resolve(fetchValue()).then((val) => { console.log(val); // 1 });}

如果将 then 方法附加到 fetchValue 返回的数字 1 上,则会出现以下错误:

function fetchValue() { return 1;}function fn() { fetchValue().then((val) => { console.log(val); });}fn();// ❌ Uncaught TypeError: fetchValue(...).then is not a function

最后值得一提的是,从异步函数返回的任何内容始终是 Promise:

Object.prototype.toString.call((async function () {})()); // '[object Promise]'5.async/await 提供更好的错误 Stack Trace

V8 工程师 Mathias 写了一篇名为 《异步 Stack Trace:为什么 await 击败 Promise#then()》 的文章,介绍了为什么与 Promise 相比,引擎更容易捕获和存储 async/await 的 Stack Trace。

通过遵循以下建议,使 JavaScript 引擎能够以更高性能、更高内存效率的方式处理 Stack Trace:

比起底层 Promise,建议使用 async/await。使用 @babel/preset-env 来避免不必要的 async/await 转译,除非确实需要

比如下面的 async/await 和 Promise 的示例,明显看出前者会抛出更多错误信息。但是值得注意的是,捕获 Stack Trace 需要时间(即降低性能),且存储堆 Stack Trace 需要内存。

async function foo() { await bar(); return 'value';}function bar() { throw new Error('BEEP BEEP');}foo().catch((error) => console.log(error.stack));// 抛出错误:Error: BEEP BEEP// at bar (<anonymous>:7:9)// at foo (<anonymous>:2:9)// at <anonymous>:10:1

async 版本确实能正确捕获错误 Stack Trace,接下来看看 Promise 版本:

function foo() { return bar().then(() => 'value');}function bar() {// 在 V8 中,Stack Trace 附加到返回的 Promise 之上// 当 Promise resolve 时,Stack Trace 被传递以便 then 根据需要使用// return Promise.resolve().then(() => { throw new Error('BEEP BEEP'); });}foo().catch((error) => console.log(error.stack));// 抛出错误:Error: BEEP BEEP at <anonymous>:7:11

此时 Stack Trace 丢失。虽然从匿名箭头函数切换到命名函数声明会有所帮助,但效果可能并不大:

function foo() { return bar().then(() => 'value');}function bar() { return Promise.resolve().then(function thisWillThrow() { throw new Error('BEEP BEEP'); });}foo().catch((error) => console.log(error.stack));// 抛出错误:Error: BEEP BEEP// at thisWillThrow (<anonymous>:7:11)6. 对 async/await 的常见反对意见

接下来一起看看对 async/await 的两种常见反对意见。

首先,当独立的(independent)异步函数调用可以与 Promise.all 并发处理时,async/await 可能被开发者用作不必要的顺序调用。当开发者试图混淆异步编程而没有真正理解 Promise 在幕后如何工作时,这种情况往往会发生。

第二个反对意见更加微妙, 一些函数式编程爱好者(Functional Programming Enthusiasts)认为 async/await 会引发命令式编程(Imperative Style Programming)。 从 FP 程序员的角度来看,能够使用循环和 try catch 并不是一件好事,因为这些语言构造意味着副作用(Side Effect)并鼓励不太理想的错误处理。

我(代指 Zhenghao)对这个论点表示同情,FP 程序员理所当然地关心程序的确定性,即希望对自己编写的代码绝对有信心。 为了实现这一目标,需要一个具有 Result 等类型的复杂类型系统,但我不认为 async/await 本身与 FP 不兼容。 曾经一位 FP 专家说 Haskell 中有一个相当于 async/await 的功能 ,即 Do-notation 。

const Result = require('folktale/result');

什么是 Result:是对可能失败的操作结果进行建模的数据结构,有助于表示错误并传播错误,为用户提供比 try...catch 等结构提供的更可控的排序操作形式。

不管怎样,我依然认为对于大多数开发者来说,FP 仍然是一种后天习得的能力(尽管我确实认为 FP 超级酷,而且我正在慢慢学习)。 async/await 提供的普通控制流语句和 try...catch 错误处理对于在 JavaScript 中编排复杂的异步操作非常有用,这也正是为什么说 “async/await 只是一个语法糖” 是一种轻描淡写的说法。

参考资料

https://www.zhenghao.io/posts/await-vs-promise

https://mathiasbynens.be/notes/async-stack-traces

https://dev.to/jesterxl/why-i-don-t-use-async-await-4amc

https://folktale.origamitower.com/api/v2.3.0/en/folktale.result.html

https://www.youtube.com/watch?app=desktop&v=TY4JfLkXs-o

https://codewithandrea.com/articles/flutter-exception-handling-try-catch-result-type/

0 阅读:0

前有科技后进阶

简介:感谢大家的关注