再次学习async/await

在 JavaScript 发展历程中,异步编程经历了回调函数、Promise、async/await 三个主要阶段。2017 年,ECMAScript 2017 标准正式引入了 async/await 语法,这被誉为 JavaScript 异步编程的 "终极解决方案"。它通过一种更简洁、更直观的方式来处理异步操作,使异步代码看起来几乎和同步代码一样,极大地提高了代码的可读性和可维护性。

一、接下来我们就看一下,这三种方法做同一件事,三种写法的对比,如下所示:

// 场景:获取用户 → 获取订单 → 获取详情

// 写法1:回调地狱 😈
getUser(id, (user) => {
    getOrders(user.id, (orders) => {
        getDetail(orders[0].id, (detail) => {
            console.log(detail);
        });
    });
});

// 写法2:Promise 链 😊
getUserAsync(id)
    .then(user => getOrdersAsync(user.id))
    .then(orders => getDetailAsync(orders[0].id))
    .then(detail => console.log(detail))
    .catch(err => console.error(err));

// 写法3:async/await 😍
async function loadData() {
    const user = await getUserAsync(id);
    const orders = await getOrdersAsync(user.id);
    const detail = await getDetailAsync(orders[0].id);
    console.log(detail);
}

 async/await 则是在 Promise 基础上构建的更高层次的抽象,提供了一种 "同步式" 的异步编程体验。

二、基础关键字

1. async 定义异步函数

async def 声明异步函数,普通 def 同步函数不能用 await。
// 普通函数
function normal() {
    return "hello";
}

// async 函数——返回值自动包装成 Promise
async function asyncFunc() {
    return "hello";
}

normal();     // "hello"(普通值)
asyncFunc();  // Promise { "hello" }(自动包装成 Promise)
async function example() {
  return 42; // 实际上会返回 Promise.resolve(42)
}
# 异步函数(调用后返回协程对象,不会立刻执行)
async def demo():
    print("异步任务开始")

上面三段示例代码说明:直接调用 demo() 不会运行函数内部代码,只会生成协程 (coroutine) 对象。即使你 return 一个普通值,JS 也会自动包一层 Promise。

2. await 等待异步操作

只能写在 async def 函数内部,作用:
  • 暂停当前协程,让出 CPU
  • 等待 IO 操作完成,期间 CPU 去执行其他协程
import asyncio

async def task():
    print("开始等待")
    # 等待异步休眠,不阻塞主线程
    await asyncio.sleep(2)
    print("等待结束")

解析一下上面代码的含义:

2.1. 协程(Coroutine)的“惰性”

当你写下 async def task(): 时,你并不是在定义一个普通的函数,而是在定义一个协程
  • 如果你直接调用 task(),它不会立刻执行里面的代码,而是返回一个协程对象(coroutine object)。
  • 它必须被 await 或者交给事件循环(如 asyncio.run())才能真正运行。

2.2. await 的魔法:让出控制权

这是异步执行的核心。当代码执行到 await asyncio.sleep(2) 时,发生了以下事情:
  • 挂起(Suspend):当前协程(task)主动暂停了自己的执行,并让出(yield)了 CPU 的控制权。
  • 非阻塞等待:在这 2 秒钟里,主线程并没有被卡住。事件循环(Event Loop)会去检查有没有其他可以运行的任务。如果有,就去执行别的任务;如果没有,它就可以去处理网络 I/O 等其他事件。
  • 恢复(Resume):2 秒钟后,事件循环会重新唤醒这个协程,从 await 的下一行继续往下执行。

三、async/await 的执行顺序

核心规则先记住:
  1. async def 函数调用后不会立刻执行内部代码,只生成协程对象;
  2. 遇到 await 才会暂停当前协程,把控制权交还给事件循环,去跑其他可执行任务;
  3. 没有 await 的代码是同步顺序执行,从上到下一行一行走;
  4. 多个任务并发时,谁先结束 await 谁先恢复执行。

3.1、单协程:无并发,纯顺序

示例 1:单个 async 函数逐行执行

import asyncio

async def foo():
    print("1")
    await asyncio.sleep(2)  # 暂停,让出循环
    print("2")
    print("3")

asyncio.run(foo())
执行顺序:
  1. asyncio.run(foo()) 启动事件循环,执行协程 foo
  2. 打印 1
  3. 碰到 await sleep(2) → 暂停 foo,事件循环无事可做,等待 2 秒
  4. 休眠结束,恢复 foo,依次打印 23
输出顺序固定:
1
# 等待2秒
2
3

3.2、两个协程并发:await 会切换任务(重点)

示例 2:gather 并发两个任务

import asyncio

async def taskA():
    print("A1")
    await asyncio.sleep(2)
    print("A2")

async def taskB():
    print("B1")
    await asyncio.sleep(1)
    print("B2")

async def main():
    await asyncio.gather(taskA(), taskB())

asyncio.run(main())
 

逐步骤执行时序(关键切换逻辑)

  1. gather 同时把 taskA、taskB 交给事件循环
  2. 先运行 taskA:打印 A1 → 遇到 await sleep (2),暂停 A,交出控制权
  3. 循环切换到 taskB:打印 B1 → 遇到 await sleep (1),暂停 B
  4. 现在两个协程都在等待休眠,循环等待计时器
  5. 1 秒后:B 的 sleep 完成,恢复 taskB → 打印 B2,B 执行完毕
  6. 再过 1 秒(总 2 秒):A 的 sleep 完成,恢复 taskA → 打印 A2
标准输出顺序:
A1
B1
# 1秒后
B2
# 再等1秒
A2
 

关键结论

不是先把 A 全部跑完再跑 B;只要遇到 await,就会切走执行其他就绪协程。

3.3、await 顺序决定并发与否

情况 1:顺序 await(串行,总耗时叠加)

async def main():
    print("开始")
    await taskA()  # 等A全部做完才走下一行
    await taskB()
 
执行流程:A 完整跑完(2s)→ B 完整跑完(1s),总耗时 3s,没有并发

情况 2:先创建任务再 await(真正并发)

两种写法等价:
  1. asyncio.gather(t1,t2)
  2. create_task 提前调度
async def main():
    t1 = asyncio.create_task(taskA())  # 立刻加入循环调度
    t2 = asyncio.create_task(taskB())
    await t1
    await t2
 
总耗时 = 最长任务时长 2s。
 

3.4、易错误区:调用异步函数不 await,代码完全不跑

async def test():
    print("hello")

def main():
    test()  # 仅生成协程对象,内部print永不执行
 
只有被 await / create_task / gather 包裹,协程代码才会进入事件循环执行。
 
3.5、嵌套 await 顺序
async def inner():
    print("inner start")
    await asyncio.sleep(1)
    print("inner end")

async def outer():
    print("outer start")
    await inner()
    print("outer end")

asyncio.run(outer())

顺序:

outer start → inner start → 等 1 秒 → inner end → outer end

嵌套 await 是串行等待,内层全部完成才会走外层后续代码。

3.6、终极执行顺序挑战

一个常见的误区是认为 await 会阻塞代码执行,像同步操作一样立即执行后续代码。但实际上,await 的执行机制与事件循环和微任务队列密切相关。

以下面的代码为例:

console.log("A");
setTimeout(() => {
  console.log("B");
}, 0);
(async () => {
  console.log("C");
  await null;
  console.log("D");
})();
console.log("E");

实际输出结果是:

A
C
E
D
B

这表明:

  1. "A" 和 "C" 是同步输出的
  1. await null 会创建一个微任务
  1. 当前同步代码执行完毕后,才会执行微任务中的 "D"
  1. 最后执行宏任务中的 "B"

四、async/await 的底层实现原理

4.1、基础概念

  1. 定义:使用 function* 声明的函数,叫生成器函数;调用它不会执行函数体,而是返回一个迭代器对象(Iterator)
  2. 核心关键字 yield:暂停函数执行,并向外产出一个值;下次调用 .next() 时从暂停处恢复运行。
  3. 特点:分段执行、可暂停、可传参、可迭代、配合 async/await 早期实现异步。

基础语法

// 生成器函数,带 *
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

// 获取迭代器
const it = gen();
console.log(it.next()); // { value:1, done:false }
console.log(it.next()); // { value:2, done:false }
console.log(it.next()); // { value:3, done:false }
console.log(it.next()); // { value:undefined, done:true }
  • .next() 返回对象:{ value: 产出值, done: 是否执行完毕 }
  • done: true 代表函数执行结束。

4.2、执行顺序(重点)

生成器是手动分段暂停,执行流程:
  1. gen() 仅创建迭代器,函数内部代码一行都不跑
  2. 第一次 it.next():从函数开头执行,直到遇到第一个 yield,暂停;
  3. 第二次 it.next():从上次暂停的 yield 后继续执行,直到下一个 yield
  4. 无后续 yield 时,函数运行结束,done=true
示例拆解:
function* demo() {
  console.log('第一段代码');
  yield 'a';
  console.log('第二段代码');
  yield 'b';
  console.log('结束');
}
const it = demo();
console.log('创建迭代器,未执行内部');

let r1 = it.next(); 
// 输出:第一段代码,暂停在 yield a
console.log(r1); // {value:'a', done:false}

let r2 = it.next();
// 输出:第二段代码,暂停在 yield b
console.log(r2); // {value:'b', done:false}

let r3 = it.next();
// 输出:结束,无yield
console.log(r3); // {value:undefined, done:true}

4.3、next 传参(向生成器内部注入数据)

next(参数) 的参数会作为当前暂停 yield 的返回值
function* gen() {
  const msg1 = yield '第一步';
  console.log('收到外部参数:', msg1);
  const msg2 = yield '第二步';
  console.log('收到外部参数:', msg2);
}
const it = gen();

it.next();       // 启动,无法给第一个yield传参
it.next('hello');// msg1 = 'hello'
it.next('world');// msg2 = 'world'

4.4、return () 终止生成器

手动提前结束迭代,后续 next 永远 done:true
function* g() {
  yield 1;
  yield 2;
}
const it = g();
it.next();        // {value:1,done:false}
it.return('end'); // {value:'end', done:true}
it.next();        // {value:undefined,done:true}

4.5、throw () 在生成器内部抛出错误

function* g() {
  try {
    yield 1;
  } catch (err) {
    console.log('捕获错误', err);
  }
}
const it = g();
it.next();
it.throw(new Error('出错了'));
 

4.6、生成器可迭代(for...of/ 展开运算符)

生成器迭代器实现了 Symbol.iterator,可以直接循环:
function* gen() {
  yield 10;
  yield 20;
  yield 30;
}
// for...of 自动调用next,直到done=true
for (const v of gen()) {
  console.log(v); // 10 20 30
}

// 展开数组
const arr = [...gen()]; // [10,20,30]

五、async/await 与 Generator 的关系

5.1 async/await 本质上是 Generator 函数的语法糖,它结合了 Generator 函数和 Promise 的特性,通过状态机和自动执行器来实现 "异步转同步" 的效果。

具体来说,async/await 的实现可以理解为以下几个关键部分的组合:

  1. Generator 函数:用于实现函数的暂停和恢复执行功能。
  1. Promise 自动执行器:负责在 Promise 解决后恢复 Generator 函数的执行。
  1. 事件循环调度:协调异步操作的执行顺序。

可以说,async/await = Generator 函数 + Promise 自动执行器 + 事件循环调度。

5.2、要搞懂函数为何能暂停和恢复,那你首先要了解协程的概念。
【什么是协程?】
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程,比如当前执行的是A协程,要启动B协程,那么A协程就需要将主线程的控制权交给B协程,这就体现在A协程暂停执行,B协程恢复执行;同样,也可以从B协程中启动A协程。通常,如果从A协程启动B协程,我们就把A协程称为B协程的父协程

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换 那样消耗资源。
如图:
运行流程

1、通过调用生成器函数generateSequence来创建一个协程seq,创建之后,seq协程并没有立即执行。
2、要让seq协程执行,需要通过调用seq.next。
3、当协程正在执行的时候,可以通过yield关键字来暂停seq协程的执行,并返回主要信息给父协程。
4、如果协程在执行期间,遇到了return关键字,那么JavaScript引擎会结束当前协程,并将return后面的内容返回给父协程。
【注意:】
第一点:seq协程和父协程是在主线程上交互执行的,并不是并发执行的,它们之前的切换是通过yield和seq.next来配合完成的。
第二点:当在seq协程中调用了yield方法时,JavaScript引擎会保存seq协程当前的调用栈信息,并恢复父协程的调用栈信息。同样,当在父协程中执行seq.next时,JavaScript引擎会保存父协程的调用栈信息,并恢复seq协程的调用栈信息。

5.3、在 async/await 出现前,用 Generator + Promise 实现同步写法的异步。

第一种封装自动执行器:

function run(genFn) {
  const it = genFn();
  function next(val) {
    const res = it.next(val);
    if (res.done) return Promise.resolve(res.value);
    // 如果value是Promise,等待完成再传入下一次next
    return Promise.resolve(res.value).then(data => next(data));
  }
  return next();
}

// 异步业务
function fetchData() {
  return new Promise(res => setTimeout(() => res('接口数据'), 1000));
}

// 生成器书写异步,写法类似async
function* task() {
  const data = yield fetchData();
  console.log(data);
}

run(task);

等价于现代写法:

async function task() {
  const data = await fetchData();
  console.log(data);
}
task();

 第二种封装自动执行器:

// 转换后的状态机(简化版)
function* stateMachine() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  return 3;
}
// 自动执行器(简化版)
function execute(stateMachine) {
  const iterator = stateMachine();
  function next(value) {
    const result = iterator.next(value);
    if (result.done) return result.value;
    return result.value.then(data => next(data));
  }
  return next();
}
// execute(stateMachine) 的返回值是 Promise<3>,可以链式调用 .then() 获取最终结果。
execute(stateMachine).then(res => console.log(res)); // 输出 3

等价于现代写法:

// 原始async函数
async function asyncFunc() {
  await Promise.resolve(1);
  await Promise.resolve(2);
  return 3;
}

六、总结:async/await(执行流程分析)

虽然生成器已经能很好地满足我们的需求了,但是在ES7中又引入了async/await,这种方式能够彻底告别执行器和生成器,实现更加直观简洁的代码。其实async/await技术背后的秘密就是Promise和生成器应用,往底层说就是微任务和协程应用。要搞清楚async和await的工作原理,我们就得对async和await分开分析。
1.async
async是一个通过异步执行并隐式返回 Promise 作为结果的函数。

async function foo() {
    return 2
}
console.log(foo())  // 返回结果:Promise {<resolved>: 2}

执行这段代码,我们可以看到调用async声明的foo函数返回了一个Promise对象,状态是resolved

2. await
我们知道了async函数返回的是一个Promise对象,那下面我们再结合文中这段代码来看看await到底是什么。

async function foo() {
    console.log(1)
    let a = await 100
    console.log(a)
    console.log(2)
}
console.log(0)
foo()
console.log(3)
// 输出:
// 0
// 1
// 3
// 100
// 2

整体执行流程图:

123

结合上图,我们来一起分析下async/await的执行流程。

首先,执行console.log(0)这个语句,打印出来0。

紧接着就是执行foo函数,由于foo函数是被async标记过的,所以当进入该函数的时候,JavaScript引擎会保存当前的调用栈等信息,然后执行foo函数中的console.log(1)语句,并打印出1。

接下来就执行到foo函数中的await 100这个语句了,这里是我们分析的重点,因为在执行await 100这个语句时,JavaScript引擎在背后为我们默默做了太多的事情,那么下面我们就把这个语句拆开,来看看JavaScript到底都做了哪些事情。

当执行到await 100时,会默认创建一个Promise对象,代码如下所示:

let promise_ = new Promise((resolve,reject){
  resolve(100)
})

在这个promise_对象创建的过程中,我们可以看到在函数中调用了resolve函数,JavaScript引擎会将该任务提交给微任务队列.
然后JavaScript引擎会暂停当前协程的执行,将主线程的控制权转交给父协程执行,同时会将promise_对象返回给父协程。
主线程的控制权已经交给父协程了,这时候父协程要做的一件事是调用promise_.then来监控promise状态的改变。

接下来继续执行父协程的流程,这里我们执行console.log(3),并打印出来3。随后父协程将执行结束,在结束之前,会进入微任务的检查点,然后执行微任务队列,微任务队列中有resolve(100)的任务等待执行,执行到这里的时候,会触发promise_.then中的回调函数,如下所示:

promise_.then((value)=>{
   //回调函数被激活后
  //将主线程控制权交给foo协程,并将vaule值传给协程
})

该回调函数被激活以后,会将主线程的控制权交给foo函数的协程,并同时将value值传给该协程。

foo协程激活之后,会把刚才的value值赋给了变量a,然后foo协程继续执行后续语句,执行完成之后,将控制权归还给父协程。

以上就是await/async的执行流程。正是因为async和await在背后为我们做了大量的工作,所以我们才能用同步的方式写出异步代码来。

 

注:

参考文章如下
详解async/await原理(用同步的方式去写异步代码)_async await原理-CSDN博客

拆解字节面试题:async/await 到底是什么?底层实现 + 最佳实践全解析一、async/await:异步编程的终 - 掘金

 

 

 

 

 

posted @ 2026-07-06 17:46  chenlight  阅读(2)  评论(0)    收藏  举报