文章

JS 异步

前情提要

参考:https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Async_JS/Introducing#耗时同步函数的问题

浏览器 API 中,有些功能强大、但很耗时的函数。

这种耗时长的同步函数,运行时会有很大问题:运行的时候,整个程序会卡着,怎么点、怎么动都没反应!

用户什么都做不了,肯定会不耐烦。得想办法解决。

你可以想到一个基本思路:

  • 耗时长的函数 “在后台继续运行”;
  • 同时,用户 / 程序本身可以继续执行比较简单的同步操作。

异步

  • mdn 概念:

异步(Asynchronous)指的是两个或多个对象或事件不同时存在或发生,也就是说,它们不是同步的。 当多个相关的,但是并不依赖于前面发生的事情完成的事情发生时,它们就是异步的。

个人理解:多个相关事件不同时发生、且并没有依次发生,且事件的发生顺序不依赖于事件代码的排序先后。

异步特征:

  • 在等待响应时不会阻塞其他进程;

    我记得我以前拿上🚽做例子,来记忆异步和 Promise 哈哈哈,🆘。。

  • ……

Promise

(见另一篇笔记)

异步函数(async/await function)

async/await 的目的在于简化使用基于 promise 的 API 时所需的语法async/await 的行为就好像搭配使用了生成器和 promise。

async/await 其实就是语法糖,它们的异步底层原理和 Promise 是一样的。所以在搞清楚 Promise 后,我们学习 async/await 的主要目的就是:如何使用,以及、如何更优雅地写异步函数?

如何使用?

相比于 promise.thenawait 只是获取 promise 的结果的一个更优雅的语法。并且也更易于读写。

……另外,我们需要在函数前面加上 async 关键字,以使它们能工作;我们需要用 await 替换掉 .then 的调用。 ——https://zh.javascript.info/async-await

async/await 只是对 Promise 的语法糖 ——让你用同步写法表达异步逻辑,读起来更直观。

注意!!:async 定义的是一个 函数 —— 不调用函数本身的话,再多的 await 也看不到异步的执行结果!

【补】async/await 与 Promise 链的思维区别

(之前就总觉得奇怪:async/await 说是替换 Promise then 即可,但写起来总感觉 “懵逼、没思路” ……)

这里先放省流总结:

思维层面 Promise then async/await
代码风格 回调驱动 流程驱动
控制权 浏览器控制“何时继续” 你控制“暂停点”
可读性 分散在多个 .then() 像写同步代码一样从上到下
思维方式 “异步任务完成后做什么” “执行到这一步时先等结果再走”

具体来讲(其实是之前思考的思路)

async/await 写法:你写代码 → 遇到 await 时“暂停执行” → 等待异步结果 → 再继续往下写逻辑

(有种 “红灯停,绿灯行” 的感觉。但传统的 Promise 会经过一个个明显的十字路口、帮你分清;async/await 代码格式更美观,但像一条直通罗马的大路,哪里停哪里行、需要自己分派得更清楚。)

——其实就是相比 Promise then ,我们需要更清楚地拆分异步任务 哪里停、哪里继续走?

我觉得:

  • Promise then 因为是一块儿一块儿的,写到一个 then 块的结尾、return 一个 Promise 给下一节时,就是在提示你 “这一块结束了!”,有始有终的每一小节,给人感觉很有条理、容易掌控(容易拆解?)
  • 但 async/await 就是在一整块儿里写完所有逻辑就像写数学大题一样、、容易乱(而 Promise then 则像短文填空)?
    • 什么时候加 await ?
    • 什么时候要传 Promise ?

    这些东西,没有清晰的函数链告诉你了。你必须自己来安排这些东西。所以容易乱?

  • Promise 链 提供了“结构性提示”(每一节明确以 .then() 为界),逻辑的分块让人能自然地理解异步的先后。
  • async/await把结构感隐藏在语法糖里,换来更接近同步的阅读体验,但同时也要求开发者主动维护逻辑的节奏(什么时候等待、什么时候返回 Promise)。

Promise 链像“搭积木”,结构清晰;

async/await 像“一口气写作文”,流畅但容易失控。

接下来会更偏 “底层分析”:

async

function xxx() 前使用,标明 “这是一个异步函数” :

1

——那你就会好奇了:为啥就在函数开头加了 5 个 ”神奇字母“ ,函数就能从同步变异步了?这其中发生了甚么??

5分钟带你彻底掌握async底层实现原理!

虽然我没看懂具体语法细节但是!我觉得主要是 3 步走:

  1. Generator(生成器)函数有个特殊机制,能让函数内部 “暂停”,之后再回到原来执行的地方;
  2. 进阶一:接着让程序能自己自动(回到暂停地点?)执行,无需人来手动开关;
  3. 进阶二:再加个 Promise,形成异步链。

这样,就能看出 fetch 所有基本功能的雏形了!是不是很简单。

  • 机制对照(看着留个印象就行):

    你总结的阶段 实际机制 核心说明
    1. Generator 能暂停函数 ✔️ Generator 函数 (function*) Generator 允许函数执行到 yield 暂停,并且下次 .next() 时从暂停点继续执行。它本质上就是一种可中断的执行上下文
    2. 自动执行(不再手动 next()) ✔️ “自执行器” (比如 co 库) 早期需要人写“自动执行器”,让 Generator 自动 .next()。这样才能让它像正常函数一样连续跑。
    3. 加 Promise,形成异步链 ✔️ async/await = Generator + Promise + 自动执行 async 函数底层其实就是用 Promise 包装、并在内部隐式调用类似 co 的逻辑,让异步操作(await 后面)暂停、恢复。
  • async/await 就像两个 “标志 / 标识符” ;当你在函数前(中)加上这个标识符时,JS 就会在进入底层编译时,将函数代码自动转换为(类似以下的格式):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
      // async 版本
      async function foo() {
        const data = await fetchData();
        console.log(data);
      }
        
      // 编译后(伪代码)
      function foo() {
        return new Promise((resolve, reject) => {
          const gen = function* () {
            try {
              const data = yield fetchData();  // 暂停点,在 await 处暂停
              console.log(data);
              resolve();  // 结束时 resolve(传递 Promise 对象-状态)
            } catch (e) {
              reject(e);
            }
          }();
        
          // 自动执行器: 帮我们自动从暂停的 await 处继续往下执行
          function step(nextF, arg) {
            ...
          }
        
          step(gen.next);
        });
      }
        
    
本文由作者按照 CC BY 4.0 进行授权