js核心之async/await的本质

先有问题再有答案

  1. 如何理解async/await?
  2. async/await有哪些使用场景?
  3. 对于不支持async/await的低版本babel如何实现的?
  4. async/await与Generator是什么关系?
  5. async/await与promise是什么关系
  6. async/await与Iterator是什么关系
  7. async/await的本质是什么?

前置文章

  1. js核心之Generator函数
  2. js核心之Iterator迭代器
  3. js三座大山之异步三promise本质

设计目的

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

分析:

调试

  1. asyncGeneratorStep 函数的作用: asyncGeneratorStep 是一个辅助函数,用于处理生成器函数的每一步执行。它接收以下参数:

    • iterator: 生成器函数的迭代器。
    • resolve: 当生成器完成时,用于解决 Promise 的回调函数。
    • reject: 当生成器抛出错误时,用于拒绝 Promise 的回调函数。
    • _next_throw: 分别是生成器的 nextthrow 方法的包装函数。
    • next: 表示当前操作是 next
    • v: 传递给生成器 next 方法的值, 也是yield的返回结果。

    当生成器函数执行到 yield 表达式时,它会暂停执行并返回一个对象,该对象包含 value 属性(yield 表达式的结果)和 done 属性(表示生成器是否完成)。asyncGeneratorStep 会根据 done 属性的值来决定是继续执行生成器的下一条 yield 表达式,还是解决 Promise。

  2. _asyncToGenerator 函数的作用: _asyncToGenerator 是一个高阶函数,它接收一个生成器函数 n 并返回一个新的函数。这个新函数在内部使用 asyncGeneratorStep 来处理生成器的每一步。当调用这个新函数时,它会创建一个 Promise,并在生成器函数中使用 _next 函数来启动生成器。每当生成器函数遇到 yield 表达式时,_next 函数会被调用,并将 yield 的结果作为参数传递给 asyncGeneratorStep。如果生成器函数抛出错误,_throw 函数会被调用来处理错误。

  3. _async 函数的作用: _async 函数是一个使用 _asyncToGenerator 转换的生成器函数。它定义了一个生成器,该生成器在每个 yield 表达式处暂停,并接受一个值,这些值最终被加和并返回。这个函数通过 _asyncToGenerator 转换后,返回了一个promise。

  4. 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的同步调用消除异步的传染性

参考

  1. es6.ruanyifeng.com/#docs/async
  2. developer.mozilla.org/zh-CN/docs/...
  3. developer.mozilla.org/zh-CN/docs/...
相关推荐
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
瑶琴AI前端1 小时前
uniapp组件实现省市区三级联动选择
java·前端·uni-app
会发光的猪。1 小时前
如何在vscode中安装git详细新手教程
前端·ide·git·vscode
别拿曾经看以后~2 小时前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死2 小时前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试3 小时前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
科技探秘人3 小时前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人3 小时前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR3 小时前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香3 小时前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel