了解JS异步语法:async/await背后的秘密

前情提要 :本文将从js的异步发展历程引出async/await的浅层认知:async/await是对promise的封装。在进一步讲解async/await是如何实现对promise的封装。

js异步编程场景,学过前端的同学,对此并不陌生。在前端知识体系中,对js异步的认识尤为重要。而编写异步代码,首要解决的难题就是异步事件的同步逻辑编写。因为往往在异步事件结束后,往往会有对异步事件结果的处理需求,这就需要我们能够掌握异步事件的结束时机。一个不可控的异步,无异于是项目里面的一个不定时炸弹。

如今大众熟悉的异步事件有:promiseasync/await。而这两者的又有什么关系,promise之前的异步又是什么样子的呢?

JS异步的发展历史

回调函数

早期的Javascript中,主要通过回调函数处理异步操作 。在完成异步任务后 ,会调用一个回调函数来处理结果。

javascript 复制代码
// 模拟异步读取文件的函数
function readFileAsync(filePath, callback) {
  setTimeout(() => {
    // 模拟异步读取文件的过程
    const fileContent = "这是文件的内容";
    // 调用回调函数,并将读取到的文件内容作为参数传递给回调函数
    callback(null, fileContent);
  }, 1000); // 模拟1秒后异步操作完成
}
// 调用异步函数,并传入回调函数来处理异步操作的结果
readFileAsync('example.txt', (error, content) => {
  if (error) {
    console.error('读取文件失败:', error);
  } else {
    console.log('文件内容:', content);
  }
});

一个回调函数是不是看着还好,那如果回调里面还有回调呢,这简直是个灾难-----回调地狱 。这时候,我们的救星-promise出现了

Promise

Promise 对象表示一个异步操作的最终完成或失败,并且可以链式调用减少回调地狱的问题。

scss 复制代码
getData().then(processData).then(render).catch(handleError);

但是 在处理多个异步操作时,仍然需要嵌套多个 Promise代码结构仍然复杂 ,有没有更好的解决方案呢?我们今天的主角:Async/await就出现了。

Async/Await

async/awaitES2017中新增的语法糖。基于 Promise,并进一步简化了异步代码的编写async 函数用来声明异步函数,内部可以使用await 关键字来等待 Promise 对象的状态变化,使得异步代码的写法更加类似于同步代码,可读性更高。

scss 复制代码
async function fetchData() {
  try {
    const data = await getData();
    const result = await processData(data);
    await render(result);
    // 后续操作...
  } catch (error) {
    handleError(error);
  }
}

Promise 和 async/await 的联系

  • Promise 是异步操作的基础,通过链式调用 .then().catch() 处理异步任务的状态和结果,较为底层。
  • async/await 是基于 Promise的语法糖,更直观易懂,使得异步代码的编写更加简洁清晰,避免了回调地狱。
  • async/await 实际上是对 Promise 的封装,内部实现也是基于 Promise 的状态和结果。

async/await底层实现原理剖析

async/await要想封装promise实现代码调用顺序的同步,必须要实现代码执行顺序的控制,为此就需要借助生成器(generator)。其实async/await看起来,像极了generator(生成器),只是生成器它不能自动迭代,只能手动触发。ECMAScript 6关于gennerators的介绍如下图:

要理解下面所说的async/await底层实现原理,就必须要理解什么是生产器,及生成器是如何使用的(这里不过多讲解)

babeljs转化代码

下面我们通过babeljs,看一下async/await是如何借助生成器实现promise的封装。

着重看babel转化后的ES5代码。

代码解读

  • _regeneratorRuntime生成器(generator)相关实现
  • asyncGeneratorStep驱动Generator的调度器 ,用来执行next()throw()等操作。
  • _asyncToGenerator把一个 generator function 转换成 async function,并返回一个 Promise
  • func = /*#__PURE__*/function () {xxx}:封装的核心逻辑

借助Generator实现封装

异步代码:

dart 复制代码
const func = async () => {
 const a = await get()
  return a
}

对应的转义代码:

javascript 复制代码
var func = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
    var a;
    return _regeneratorRuntime().wrap(function _callee$(_context) {
      while (1) switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return get();
        case 2:
          a = _context.sent;
          return _context.abrupt("return", a);
        case 4:
        case "end":
          return _context.stop();
      }
    }, _callee);
  }));
  return function func() {
    return _ref.apply(this, arguments);
  };
}();

对这段代码是不是看得云里雾里的?

1、_context 是什么?

_context 是一个对象,regeneratorRuntime 生成器状态机的核心,它会根据 next 指针的值,依次执行对应的代码块,负责存储当前生成器的执行状态,包括:

  • prev:上一条执行的指令编号。
  • next:下一条要执行的指令编号。
  • sent:存储 yield 语句的返回值。
  • done:表示生成器是否完成。

_context.abrupt(type, value) :用于提前终止生成器函数的执行,并返回一个值。

常见 abrupt 的类型

_context.abrupt(type, value) 作用
"return", value 立即返回 value 并终止执行
"throw", error 抛出错误 error
"break", label 跳出循环(通常不直接使用)
"continue", label 继续执行(通常不直接使用)

regeneratorRuntime.wrap(innerFn, outerFn, self, tryLocsList) :将一个普通的Generator函数封装成符合 regenerator运行时的标准 Generator对象。

  • innerFn:原始的 Generator 函数(即 function*)。
  • outerFn:外部 Generator 函数的构造器(如果有的话)。
  • self:执行 Generatorthis 上下文。
  • tryLocsList:存储 try/catch/finally 代码块的位置。

2、_context.prev = _context.next 作用是什么?

记录当前执行的位置,以便于在下一次 yieldawait 之后,知道上一次执行到哪一步,便于恢复状态。

在 JavaScript 运行时,async/await 被转译为生成器(generator),需要 prev 记录当前执行的 next 值,以便:

  1. await 发生时,保存执行上下文。
  2. 发生异常时,能跳转到正确的错误处理位置。
  3. 通过 next() 方法继续执行,恢复到正确的 case。

prev笔者认为可以理解为操作系统里面,系统检测到中断时,保存当前执行程序的PC指针,以便中断处理结束后回到原程序继续执行。

3、switch这样书写case对比的变量是哪个?

switch ((_context.prev = _context.next)) 这里的 case 语句是 对比 _context.next 的值

4、执行顺序梳理

到这里就可以知道程序执行顺序了,每次func()开始,最先执行case 0,遇到异步操作,修改 _context.next的值,return异步操作,等待异步操作完成,在下一个微任务队列中继续执行 case 2,知道程序结束,进入case "end",调用_context.stop(),将生成器的done置为true,从而结束整个 Generator 执行流程。

5、类比

只有一个异步操作看懂了,那么多个呢?

6、 其它关键代码,生成器自迭代实现

asyncGeneratorStep

scss 复制代码
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { 
  try {
    var info = gen[key](arg); // arg是_context.sent的值
    var value = info.value;
  } catch (error) { 
    reject(error);
    return; 
  } 
  if (info.done) {
    resolve(value); 
  } else {
    Promise.resolve(value).then(_next, _throw); 
  }
}

_asyncToGenerator

javascript 复制代码
function _asyncToGenerator(fn) { 
  return function () { 
    var self = this, args = arguments;
     return new Promise(function (resolve, reject) { 
      var gen = fn.apply(self, args); 
      function _next(value) { 
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); 
      } 
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); 
      }
      _next(undefined);// 关键,触发case 0代码的执行,迭代的开始
    }); 
  }; 
}
相关推荐
烛阴1 小时前
秒懂 JSON:JavaScript JSON 方法详解,让你轻松驾驭数据交互!
前端·javascript
拉不动的猪1 小时前
刷刷题31(vue实际项目问题)
前端·javascript·面试
zeijiershuai1 小时前
Ajax-入门、axios请求方式、async、await、Vue生命周期
前端·javascript·ajax
恋猫de小郭1 小时前
Flutter 小技巧之通过 MediaQuery 优化 App 性能
android·前端·flutter
只会写Bug的程序员1 小时前
面试之《webpack从输入到输出经历了什么》
前端·面试·webpack
拉不动的猪1 小时前
刷刷题30(vue3常规面试题)
前端·javascript·面试
狂炫一碗大米饭1 小时前
面试小题:写一个函数实现将输入的数组按指定类型过滤
前端·javascript·面试
最胖的小仙女1 小时前
通过动态获取后端数据判断输入的值打小
开发语言·前端·javascript
yzhSWJ2 小时前
Vue 3 中,将静态资源(如图片)转换为 URL
前端·javascript·vue.js
Moment2 小时前
🏞 JavaScript 提取 PDF、Word 文档图片,非常简单,别再头大了!💯💯💯
前端·javascript·react.js