再次学习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 的执行顺序
async def函数调用后不会立刻执行内部代码,只生成协程对象;- 遇到
await才会暂停当前协程,把控制权交还给事件循环,去跑其他可执行任务; - 没有
await的代码是同步顺序执行,从上到下一行一行走; - 多个任务并发时,谁先结束
await谁先恢复执行。
3.1、单协程:无并发,纯顺序
示例 1:单个 async 函数逐行执行
import asyncio
async def foo():
print("1")
await asyncio.sleep(2) # 暂停,让出循环
print("2")
print("3")
asyncio.run(foo())
asyncio.run(foo())启动事件循环,执行协程 foo- 打印
1 - 碰到
await sleep(2)→ 暂停 foo,事件循环无事可做,等待 2 秒 - 休眠结束,恢复 foo,依次打印
2→3
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())
逐步骤执行时序(关键切换逻辑)
gather同时把 taskA、taskB 交给事件循环- 先运行 taskA:打印
A1→ 遇到 await sleep (2),暂停 A,交出控制权 - 循环切换到 taskB:打印
B1→ 遇到 await sleep (1),暂停 B - 现在两个协程都在等待休眠,循环等待计时器
- 1 秒后:B 的 sleep 完成,恢复 taskB → 打印
B2,B 执行完毕 - 再过 1 秒(总 2 秒):A 的 sleep 完成,恢复 taskA → 打印
A2
A1
B1
# 1秒后
B2
# 再等1秒
A2
关键结论
await,就会切走执行其他就绪协程。3.3、await 顺序决定并发与否
情况 1:顺序 await(串行,总耗时叠加)
async def main():
print("开始")
await taskA() # 等A全部做完才走下一行
await taskB()
情况 2:先创建任务再 await(真正并发)
asyncio.gather(t1,t2)create_task提前调度
async def main():
t1 = asyncio.create_task(taskA()) # 立刻加入循环调度
t2 = asyncio.create_task(taskB())
await t1
await t2
3.4、易错误区:调用异步函数不 await,代码完全不跑
async def test():
print("hello")
def main():
test() # 仅生成协程对象,内部print永不执行
await / create_task / gather 包裹,协程代码才会进入事件循环执行。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
这表明:
- "A" 和 "C" 是同步输出的
- await null 会创建一个微任务
- 当前同步代码执行完毕后,才会执行微任务中的 "D"
- 最后执行宏任务中的 "B"
四、async/await 的底层实现原理
4.1、基础概念
- 定义:使用
function*声明的函数,叫生成器函数;调用它不会执行函数体,而是返回一个迭代器对象(Iterator)。 - 核心关键字
yield:暂停函数执行,并向外产出一个值;下次调用.next()时从暂停处恢复运行。 - 特点:分段执行、可暂停、可传参、可迭代、配合
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、执行顺序(重点)
gen()仅创建迭代器,函数内部代码一行都不跑;- 第一次
it.next():从函数开头执行,直到遇到第一个yield,暂停; - 第二次
it.next():从上次暂停的yield后继续执行,直到下一个yield; - 无后续
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 的实现可以理解为以下几个关键部分的组合:
- Generator 函数:用于实现函数的暂停和恢复执行功能。
- Promise 自动执行器:负责在 Promise 解决后恢复 Generator 函数的执行。
- 事件循环调度:协调异步操作的执行顺序。
可以说,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
整体执行流程图:

结合上图,我们来一起分析下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:异步编程的终 - 掘金

浙公网安备 33010602011771号