您是否有过这样的感觉:您在咖啡店排队等候 JavaScript 来取拿铁咖啡?异步编程常常给人这样的感觉——同时处理多个订单可能会让您陷入等待。幸运的是,promises 和 async/await 等工具可确保流程保持平稳高效,让您的代码继续运行而不会出现延迟。
在本指南中,我们将详细介绍 promise 的工作原理、引入 async/await 的原因,以及它如何简化异步代码的编写。无论您是试图掌握这些概念的初学者,还是想清楚何时使用每种方法,本文都将帮助您掌握基础知识。
什么是承诺?
promise 是 javascript 中处理异步操作的基本概念。从本质上讲,promise 代表了现在、稍后或永远可用的值。将其视为包裹的追踪号码:虽然您还没有收到包裹,但追踪号码让您确信包裹正在运送途中(或者让您知道是否出现问题)。
基于“现在、以后或永远”的叙述,promise 实际上在以下三种状态之一中运行:
立即学习“Java免费学习笔记(深入)”;
- pending:异步操作尚未完成。
- 已完成:操作已成功完成,promise 现在保存结果。
- 已拒绝:出了点问题,promise 提供了一个错误。
创建和使用 promise 涉及一个简单的 api。以下是定义 promise 的方法:
const fetchdata = new promise((resolve, reject) => { settimeout(() => { const data = { id: 1, name: "javascript basics" }; resolve(data); // simulates a successful operation // reject("error: unable to fetch data"); // simulates a failure }, 1000); });
要处理结果,您可以将 .then()、.catch() 和 .finally() 方法链接到 promise 对象:
fetchdata .then((data) => { console.log("data received:", data); }) .catch((error) => { console.error(error); }) .finally(() => { console.log("operation complete."); });
当 promise 解析成功时执行 then() 方法中的回调。 .catch() 方法中的回调在 promise 解析失败时执行,finally() 方法中的回调在 promise 解析后执行,无论解析结果如何。
promise 的好处
promise 为深层嵌套的回调(通常称为“回调地狱”)提供了一种更干净的替代方案。 promise 允许链接,而不是堆叠回调,从而使操作流程更易于遵循:
dotask1() .then((result1) => dotask2(result1)) .then((result2) => dotask3(result2)) .catch((error) => console.error("an error occurred:", error));
如果使用传统回调编写相同的代码,它会是什么样子:
dotask1((error1, result1) => { if (error1) { console.error("an error occurred:", error1); return; } dotask2(result1, (error2, result2) => { if (error2) { console.error("an error occurred:", error2); return; } dotask3(result2, (error3, result3) => { if (error3) { console.error("an error occurred:", error3); return; } console.log("final result:", result3); }); }); });
令人困惑,不是吗?这就是为什么 promise 在引入时就改变了 javascript 编码标准。
promise 的缺点
虽然 promises 极大地改进了传统的回调函数,但它们也面临着自己独特的挑战。尽管有这些好处,但它们在复杂的场景中可能会变得笨拙,导致代码冗长和调试困难。
即使使用 .then() 链接,promise 在处理多个异步操作时也会导致代码混乱。例如,使用 .then() 块管理顺序操作和使用 .catch() 进行错误处理可能会让人感觉重复且难以理解。
dotask1() .then((result1) => dotask2(result1)) .catch(task1error, (error) => console.error("an error occurred in task 1:", error)); .then((result2) => dotask3(result2)) .catch(task2error, (error) => console.error("an error occurred in task 2:", error)); .catch((error) => console.error("an error occurred in the chain:", error));
虽然比嵌套回调更清晰,但链接语法仍然很冗长,特别是在需要详细的自定义错误处理逻辑时。此外,忘记在链的末尾添加 .catch() 可能会导致静默失败,从而使调试变得棘手。
此外,promises 中的堆栈跟踪不如同步代码中的堆栈跟踪那么直观。发生错误时,堆栈跟踪可能无法清楚地指示异步流程中问题的根源。
最后,虽然 promise 有助于减少回调地狱,但当任务相互依赖时,它们仍然会导致复杂性。嵌套的 .then() 块可以在某些用例中重新出现,带回一些它们本来要解决的可读性挑战。
输入异步/等待
随着 es2017 (es8) 中 async/await 的引入,javascript 中的异步编程取得了巨大的飞跃。 async/await 构建在 promises 之上,允许开发人员编写外观和行为更像同步代码的异步代码。这使其成为真正的游戏规则改变者,可以提高可读性、简化错误处理并减少冗长。
什么是异步/等待?
async/await 是一种旨在使异步代码更易于理解和维护的语法。
async 关键字用于声明一个始终返回 promise 的函数。在此函数中,await 关键字暂停执行,直到 promise 得到解决或拒绝。这会产生线性且直观的流程,即使对于复杂的异步操作也是如此。
下面是 async/await 如何简化您在上面看到的相同代码示例的示例:
async function executetasks() { try { const result1 = await dotask1(); const result2 = await dotask2(result1); const result3 = await dotask3(result2); console.log("all tasks completed successfully:", result3); } catch (error) { if (error instanceof task1error) { console.error("an error occurred in task 1:", error); } else if (error instanceof task2error) { console.error("an error occurred in task 2:", error); } else { console.error("an error occurred in the chain:", error); } } } executetasks();
async/await 消除了对 .then() 链的需要,允许代码按顺序流动。这使得遵循逻辑变得更容易,特别是对于需要依次执行的任务。
对于 promise,必须使用 .catch() 在链的每个级别捕获错误。另一方面,async/await 使用 try/catch 整合错误处理,减少重复并提高清晰度。
async/await 产生比 promise 更直观的堆栈跟踪。当发生错误时,跟踪会反映实际的函数调用层次结构,从而减少调试的麻烦。总的来说,async/await 感觉更“自然”,因为它与同步代码的编写方式一致。
比较 promise 和 async/await
正如您已经看到的,async/await 在可读性方面表现出色,尤其是对于顺序操作。 promise 及其 .then() 和 .catch() 链接很快就会变得冗长或复杂。相反,异步/等待代码更容易理解,因为它模仿同步结构。
灵活性
promise 仍然有一席之地,特别是对于并发任务。 promise.all() 和 promise.race() 等方法对于并行运行多个异步操作更有效。 async/await 也可以处理这种情况,但需要额外的逻辑才能达到相同的结果。
// A Promise.all() example Promise.all([task1(), task2(), task3()]) .then((results) => console.log("All tasks completed:", results)) .catch((error) => console.error("An error occurred:", error));
错误处理
虽然使用单个 .catch() 进行集中式错误处理对于线性 promises 链效果很好,但建议对跨链的不同错误类型使用分布式 .catch 调用,以获得最佳可读性。
另一方面,try/catch 块为处理错误提供了更自然的结构,特别是在处理顺序任务时。
表现
就性能而言,async/await 本质上等同于 promises,因为它是建立在 promises 之上的。然而,对于需要并发的任务,promise.all() 可以更高效,因为它允许多个 promise 并行执行,如果任何 promise 拒绝,则快速失败。
何时使用哪个
如果您的任务涉及大量并发操作,例如同时从多个 api 获取数据,promise 很可能是更好的选择。如果您的异步代码不涉及大量链接,那么 promise 也非常适合这种情况,因为它很简单。
另一方面,async/await 在需要顺序执行大量任务或优先考虑可读性和可维护性的情况下表现出色。例如,如果您有一系列相关操作,例如获取数据、转换数据和保存数据,则 async/await 提供干净且同步的结构。这使得跟踪操作流程变得更加容易,并通过 try/catch 块简化了集中式错误处理。 async/await 对于初学者或优先考虑可读代码的团队特别有用。
结论
javascript 提供了两个强大的工具来管理异步操作:promises 和 async/await。 promise 彻底改变了开发人员处理异步任务的方式,解决了回调地狱和启用链接等问题。 async/await 建立在 promises 的基础上,提供了更清晰的语法,让人感觉更加自然和直观,特别是对于顺序任务。
既然您已经探索了这两种方法,您就可以选择最适合您需求的一种了。尝试将基于 promise 的函数转换为 async/await 并观察可读性的差异!
有关更多信息,请查看 mdn promise 文档或尝试交互式编码沙箱!