该系列是本人准备面试的笔记,或许有描述不当的地方,请在评论区指出,感激不尽。
其他篇章:
- Promise.try 和 Promise.withResolvers,你了解多少呢?
- 挑战ChatGPT提供的全网最复杂“事件循环”面试题
- Vue.nextTick 从v3.5.13追溯到v0.7.0
- Vue 怎么监听 Set,WeakSet,Map,WeakMap 变化?
- Vue 是怎么从<HelloWorld />、<component is='HelloWorld'>找到HelloWorld.vue
前言
上一篇,《【面试准备】Promise.try 和 Promise.withResolvers,你了解多少呢?》介绍了 Promise 的前世今生,今天来看看 async/await 的来龙去脉。
为什么有了 Promise,还会诞生 async/await
地球人都知道,在 JavaScript 中,Promise 的引入是为了更优雅地解决回调地狱问题。它通过 .then
和 .catch
链式调用,使异步操作的可读性大大提升。然而,Promise 的链式调用依旧存在以下问题:
- 语法复杂性:即使链式调用已经简化了回调嵌套,但在多个异步任务间处理逻辑仍显冗长,特别是有条件或循环时,代码很快会变得难以维护。
fetchData()
.then((data) => processData(data))
.then((processedData) => saveData(processedData))
.catch((err) => handleError(err));
- 错误处理局限:在 Promise 链中,必须特别注意在哪一步处理错误,稍有遗漏可能导致异常未捕获。
- 无法直观表示顺序:链式结构虽然流畅,但不如同步代码那样清晰直观,尤其是在涉及多个异步任务时。
因此,async/await 应运而生。它在 Promise 基础上,进一步优化了语法,使异步代码的写法更加接近同步逻辑,增强了代码的可读性。
async/await 特点
- 语法简洁,结构清晰:
async/await
提供了更直观的代码书写方式,让异步代码看起来像同步代码,从而提高了代码的可读性。 - 基于 Promise 工作:
async
函数始终返回一个 Promise 对象。await
用于等待一个返回 Promise 的表达式完成,并获取其解析结果。 - 异步函数自动包装:在
async
函数中,无需手动创建和管理 Promise,async
会自动将函数的返回值封装为 Promise。 - 错误处理更加方便:使用
try...catch
可以直接捕获异步操作中的错误,无需像传统 Promise 那样单独使用catch
方法。 - 局限性:
await
必须在async
函数中使用,不能单独在顶层代码中使用(虽然模块化支持顶层await
)。- 如果不小心,可能导致性能问题。例如,多个不依赖的异步任务被按顺序处理,而非并发执行。
关于 Top-level await
,可点击链接查看具体提案,或者浏览《(建议收藏) 深入了解 Top-level await》。
babel 编译
async 是 ECMAScript 2017 (ES8) 引入的关键字,那它隐藏在底层的逻辑是怎么样的呢?让我们跟着 babel 走进它的内部世界,见识最原始的 async/await
。
我们用 babel 的 Try it out 编译以下代码:
function resolveAfter2Seconds() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling'); // first
const result = await resolveAfter2Seconds();
console.log(result); // third
}
function syncCall() {
asyncCall();
console.log('sync'); // second
}
syncCall();
- 首先,可以看到上述逻辑被编译为以下代码:
function resolveAfter2Seconds() {
return new Promise(function (resolve) {
setTimeout(function () {
resolve('resolved');
}, 2000);
});
}
function asyncCall() {
return _asyncCall.apply(this, arguments);
}
function _asyncCall() {
_asyncCall = _asyncToGenerator(/*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
var result;
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
console.log('calling'); // first
_context.next = 3;
return resolveAfter2Seconds();
case 3:
result = _context.sent;
console.log(result); // third
case 5:
case "end":
return _context.stop();
}
}, _callee);
}));
return _asyncCall.apply(this, arguments);
}
function syncCall() {
asyncCall();
console.log('sync'); // second
}
syncCall();
Okay,我们一步步来看调用情况。
- 首先毋庸置疑是 syncCall -> asyncCall -> _asyncCall。
- 接着看
_asyncCall
内部逻辑,调用_asyncToGenerator
,接收一个参数,该参数由_regeneratorRuntime().mark
函数调用生成 _regeneratorRuntime().mark
这个方法将一个普通函数标记为生成器函数,表示它包含了异步操作,并且返回一个包含状态机的对象。_regeneratorRuntime().mark
方法接收一个函数,该函数返回了_regeneratorRuntime().wrap
的结果。_regeneratorRuntime().wrap
将生成器函数包裹起来,创建一个可以正确处理异步流程的状态机。通过yield
来处理异步操作,并且在每次await
时暂停执行,等待异步操作的结果。- 正式进入执行代码的流程,首先打印
'calling'
,表示函数已被调用。 _context.next = 3
之后,yield resolveAfter2Seconds()
会调用一个异步函数(假设resolveAfter2Seconds
是一个返回Promise
的函数)。这个yield
操作会暂停执行,并等待resolveAfter2Seconds
完成后继续执行。- 在等待
resolveAfter2Seconds
完成的过程中,会继续打印'sync'
。 - 当
resolveAfter2Seconds
解析完成后,_context.sent
获取到异步操作的结果并存储到result
变量中。 - 执行完
await
后,console.log(result)
会输出异步操作的结果,resolveAfter2Seconds
返回了'resolved'
,所以最终控制台会输出'resolved'
。 return _context.stop()
会结束生成器的执行,确保函数的执行完毕。
其中 _asyncToGenerator
加 asyncGeneratorStep
会不断调用 next
,直到 done
为 true 后,将结果 resolve
,如果中间 catch
到错误或者 Generator 调用了 throw
,则 reject
。
- _asyncToGenerator:将一个生成器函数转换为返回 Promise 的异步函数。
- 包装生成器函数
n
,使其在异步函数上下文中运行。 - 调用
asyncGeneratorStep
函数逐步执行生成器的next
或throw
操作。
- 包装生成器函数
function _asyncToGenerator(n) {
return function () {
var t = this,
e = arguments
return new Promise(function (r, o) {
var a = n.apply(t, e)
function _next(n) {
asyncGeneratorStep(a, r, o, _next, _throw, 'next', n)
}
function _throw(n) {
asyncGeneratorStep(a, r, o, _next, _throw, 'throw', n)
}
_next(void 0)
})
}
}
- asyncGeneratorStep: 处理生成器的步骤,基于其当前状态返回值或抛出错误。
- 调用生成器的特定方法(
next
或throw
),并根据结果判断生成器是否完成。 - 若未完成,则包装返回值为一个 Promise,确保异步操作链的正确性。
- 调用生成器的特定方法(
function asyncGeneratorStep(n, t, e, r, o, a, c) {
try {
var i = n[a](c),
u = i.value
} catch (n) {
return void e(n)
}
i.done ? t(u) : Promise.resolve(u).then(r, o)
}
-
_regeneratorRuntime:提供了生成器和异步函数运行所需的核心支持,是
Generator
的 polyfill。wrap
: 该函数将一个生成器函数(或打算作为生成器的函数)包装成一个实际的生成器,使用Context
对象管理函数执行的状态。这对于在生成器执行期间保持其状态至关重要。Generator
: 代表生成器函数的原型,使其能够以暂停和恢复的方式执行代码。makeInvokeMethod
: 该方法处理生成器内部逻辑的调用,控制生成器的流程,处理next
、throw
和return
操作。AsyncIterator
: 这是一个处理异步迭代的类。它定义了如何处理异步函数中的yield
。如果从异步生成器返回的值是一个 Promise,代码会先解析它,再继续执行。GeneratorFunction
和GeneratorFunctionPrototype
定义了生成器函数的结构和原型链。AsyncIterator
是一个用于处理异步迭代的类,它使得生成器可以返回 Promise 并等待它们完成。Context
: 上下文对象,对于管理生成器的执行状态至关重要。它跟踪当前的状态(如next
、throw
、return
)、try/catch
块和生成器函数的流程控制。它确保生成器能够从停止的地方恢复执行。dispatchException
和abrupt
: 这些方法帮助处理错误并控制生成器的流程(例如,当抛出异常时,它会沿着生成器的执行路径传播)。
Tips: 建议将 babel 编译后的代码复制到 VSCode 或者在浏览器运行,在 syncCall()
断点,借助 Step Into
一步步调用。
Generator
说实话,上面 babel 编译 Generator 的 polyfill,看得我一脸懵逼,脑子嗡嗡的。
看完两位大佬的文章《ES6 系列之 Babel 将 Generator 编译成了什么样子》和《通过babel学习Generator的底层实现》,稍微懂了一点。
在 Promise 出现之前,处理异步操作的传统方式是嵌套回调,而 Generator 提供了另一种解决方案,通过暂停函数执行使其具有更高的可控性。
Generator 的优势:
- 暂停和恢复执行:允许函数在中途暂停,并在后续重新启动。
- 可组合性:与 Promise 搭配可以实现简化的异步流控制。
使用 function*
定义,调用后不会立即执行,而是返回一个迭代器对象。
- 通过
yield
暂停函数执行。 - 使用
next()
恢复执行并传递参数。
async/await 与 Generator 的对比
async/await
是对 Generator + Promise 的语法糖。async
函数实际上是使用了 Promise,而 await
的行为类似于 yield
,但更加简洁,且无需手动管理 next
。
特性 | async/await | Generator |
---|---|---|
引入版本 | ES8 | ES6 |
定义方式 | async function | function* |
暂停机制 | await | yield |
自动化驱动 | 自动驱动 | 需要手动调用 next |
错误处理 | 支持 try/catch | 需要手动处理错误 |
返回值 | 始终返回一个 Promise | Iterator (惰性求值) |
目标用途 | 异步流控制 | 暂停函数执行 |
内部机制 | 基于 Promise 和 Generator | 基于迭代器 |
适用场景 | 更直观,专注于简化异步逻辑,适合处理复杂异步场景 | 更通用的暂停机制,适合惰性迭代 |
function* fetchData() {
try {
const data = yield fetch('https://api.example.com/data');
console.log(data);
} catch (error) {
console.error(error);
}
}
const it = fetchData();
it.next().value
.then((result) => it.next(result))
.catch((error) => it.throw(error));
async function fetchData() {
try {
const data = await fetch('https://api.example.com/data');
console.log(await data.json());
} catch (error) {
console.error(error);
}
}
实现 async/await
从上面 babel 编译后的代码可以知道,将 _asyncToGenerator
和 asyncGeneratorStep
结合起来就是答案。
function asyncToGenerator(fn) {
return function() {
const gen = fn.apply(this, arguments)
return new Promise((resolve, reject) => {
function step(key, arg) {
let result
try {
result = gen[key](arg)
} catch (error) {
return reject(error)
}
const { value, done } = result
if (done) {
return resolve(value)
} else {
return Promise.resolve(value).then(
function onResolve(val) {
step("next", val)
},
function onReject(err) {
step("throw", err)
},
)
}
}
step("next")
})
}
}
await-to-js
谈到 async/await,不得不提的一个项目就是 await-to-js。
在使用 async/await
时,常常需要编写如下代码来捕获和处理错误:
javascript
复制代码
try {
const result = await someAsyncFunction();
// 处理结果
} catch (error) {
// 处理错误
}
当代码中有多个异步操作时,这样的写法可能会显得冗长。
而且使用 try/catch
块时会引入额外的缩进,导致代码层级增加。
await-to-js
通过提供一个函数 to()
,可以更简洁地捕获异步操作的结果和潜在的错误,而不需要反复使用 try/catch
块。
import to from 'await-to-js';
async function exampleFunction() {
const [err, data] = await to(someAsyncFunction());
if (err) {
// 处理错误
console.error(err);
return;
}
// 处理成功的结果
console.log(data);
}
最后
朋友们,有没有好玩的 Generator 项目推荐,让我学习一下。我觉得 Generator 暂停函数执行的机制很酷,有很大的可玩性。
记得点赞收藏评论一键三连~