先有问题再有答案
如何理解async/await?
async/await有哪些使用场景?
对于不支持async/await的低版本babel如何实现的?
async/await与Generator是什么关系?
async/await与promise是什么关系
async/await与Iterator是什么关系
async/await的本质是什么?
前置文章
设计目的
async/await 的设计主要是为了简化JavaScript的异步编程和提高代码的可读性和可维护性。
在JavaScript中处理异步操作,早期主要依赖于回调函数(callbacks)和事件监听,但这导致代码很容易陷入"回调地狱",即回调函数套回调函数,使得代码难以阅读和理解。
后来Promise应运而生,虽然它改善了异步操作的书写方式,避免了多层的回调嵌套,但在某些复杂的异步逻辑处理中依然显得累赘。
而async/await是基于Promise实现的,它通过语言层面提供了更为优雅的异步方案。在语法上,async/await使得异步代码的书写和普通的同步代码几乎没有差别,大大提高了代码可读性,也便于开发者理解代码的执行逻辑。
同时,使用async/await也使得对异步操作的错误处理变得更为方便,可以直接使用try/catch结构进行异常处理,这与同步代码中的错误处理方式一致,从而提高代码的一致性和可维护性。
关于异步方案的发展背景可以参考这篇文章 js三座大山之异步二异步方案
babel是如何编译async/await
兼容性
编译后代码
这里需要先掌握好前置知识再来阅读编译后的代码
javascript
async function t(){
const res1 = await 1;
const res2 = await 2;
const res3 = await 3;
return res1 + res2 + res3
}
t().then(console.log); // 6
将babel目标设置为Safari 10 代码如下:
这里对参数名做了些改动便于理解。
javascript
// 定义一个处理异步生成器每一步的函数
function asyncGeneratorStep(iterator, resolve, reject, _next, _throw, next, v) {
try {
// 尝试从生成器中获取下一个值
var i = iterator[next](v);
var u = i.value;
} catch (n) {
// 如果有错误发生,则使用reject函数抛出错误
return void reject(n);
}
// 如果生成器完成,则使用resolve函数处理最后的值,否则继续进行完生成器的剩余部分
i.done ? resolve(u) : Promise.resolve(u).then(_next, _throw);
}
// 定义一个返回生成器函数的函数
function _asyncToGenerator(generator) {
return function () {
var self = this;
var arg = arguments;
return new Promise(function (resolve, reject) {
var iterator = generator.apply(self, arg);
function _next(v) {
// 在每个Promise被resolve时,调用_generator的步骤函数
asyncGeneratorStep(iterator, resolve, reject, _next, _throw, 'next', v);
}
function _throw(v) {
// 在每个Promise被reject时,调用_generator的步骤函数
asyncGeneratorStep(iterator, resolve, reject, _next, _throw, 'throw', v);
}
// 启动生成器
_next(void 0);
});
};
}
// 定义一个异步函数
function _async() {
var _a = _asyncToGenerator(function* () {
var res1 = yield 1;
var res2 = yield 2;
var res3 = yield 3;
// 返回这三个值的和
return res1 + res2 + res3;
});
// 使用apply方法调用函数,确保this和参数都正确地传递给函数
return _a.apply(this, arguments);
}
// 封装_async函数的调用
function asyncBabelPolyfill() {
return _async.apply(this, arguments);
}
// 执行函数并在Promise resolve时打印结果
asyncBabelPolyfill().then(console.log); // 输出6
分析:
调试
-
asyncGeneratorStep 函数的作用:
asyncGeneratorStep
是一个辅助函数,用于处理生成器函数的每一步执行。它接收以下参数:iterator
: 生成器函数的迭代器。resolve
: 当生成器完成时,用于解决 Promise 的回调函数。reject
: 当生成器抛出错误时,用于拒绝 Promise 的回调函数。_next
和_throw
: 分别是生成器的next
和throw
方法的包装函数。next
: 表示当前操作是next
。v
: 传递给生成器next
方法的值, 也是yield的返回结果。
当生成器函数执行到
yield
表达式时,它会暂停执行并返回一个对象,该对象包含value
属性(yield
表达式的结果)和done
属性(表示生成器是否完成)。asyncGeneratorStep
会根据done
属性的值来决定是继续执行生成器的下一条yield
表达式,还是解决 Promise。 -
_asyncToGenerator 函数的作用:
_asyncToGenerator
是一个高阶函数,它接收一个生成器函数n
并返回一个新的函数。这个新函数在内部使用asyncGeneratorStep
来处理生成器的每一步。当调用这个新函数时,它会创建一个 Promise,并在生成器函数中使用_next
函数来启动生成器。每当生成器函数遇到yield
表达式时,_next
函数会被调用,并将yield
的结果作为参数传递给asyncGeneratorStep
。如果生成器函数抛出错误,_throw
函数会被调用来处理错误。 -
_async 函数的作用:
_async
函数是一个使用_asyncToGenerator
转换的生成器函数。它定义了一个生成器,该生成器在每个yield
表达式处暂停,并接受一个值,这些值最终被加和并返回。这个函数通过_asyncToGenerator
转换后,返回了一个promise。 -
asyncBabelPolyfill: 编译后的入口函数。
总结
babel将async/await转换为Generator函数,并且会自动遍历返回的迭代器,从yield
表达式读取输入 并将结果通过next返回给调用方,然后封装为一个promise对象,当迭代器执行完毕,resolve这个promise对象,如果迭代过程中报错则reject这个promise。
所以async/await的本质我们可以理解为:async/await = Generator + 自动遍历Iterator + Promise
关于 void 0
使用 void 0
时,实际上是在调用 void
运算符并传递 0
作为参数。由于 void
运算符总是返回 undefined
,所以 void 0
的结果也是 undefined
。
使用 void 0
而不是直接写 undefined
的一个原因是,undefined可以被重新赋值为其它值,而void 0一直保持是undefined。这样可以防止在那些可以改变undefined值的环境中出错。
javascript
undefined = "new value"; // 在老的JavaScript版本中这是允许的
console.log(undefined); // 报错:new value
console.log(void 0); // 输出:undefined
上面的代码中,undefined被重新赋值后,全局的undefined就不再真正是undefined了。而void 0在任何情况下都保持是undefined,这样就避免了由于不小心改变了undefined而导致的错误。所以在某些代码中,为了写出更加安全的代码,会选择使用void 0来代替undefined。
现在的JavaScript版本已经不允许更改全局的undefined值,它现在是只读(read-only)的。对于现代版本的JavaScript(ECMAScript 5 及以后),试图给undefined赋值会被忽略(在严格模式下,试图给undefined赋值会抛出错误)。
在生成器函数或异步函数的上下文中,使用 void 0
作为参数传递给 next
方法,通常是因为你想要开始执行生成器,但不需要从 yield
表达式中接收任何值。这与传递 undefined
是等效的.
async/await的缺陷
async/await
语法在 JavaScript 中提供了一种非常直观和简洁的方式来处理异步操作,它让异步代码看起来和同步代码非常相似,从而提高了代码的可读性和可维护性。然而,async/await
有一个缺点,即所谓的"异步传染性"。
这里的"异步传染性"指的是当你在函数内部使用 await
表达式时,你必须确保这个函数本身被声明为 async
。这是因为 await
只能在 async
函数内部使用。这个要求确保了 await
表达式暂停执行的能力被正确地管理,并且任何在 await
表达式中抛出的错误都能被正确地捕获和处理。
想要解决"异步传染性"问题可以参考 这篇文章: js三座大山之异步四-Promise的同步调用消除异步的传染性