找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

320

积分

0

好友

40

主题
发表于 2025-12-26 09:08:07 | 查看: 28| 回复: 0

前面我们介绍了如何使用 Promise 解决回调地狱问题。然而,即便使用了 Promise,如果处理流程复杂,代码中仍会充斥大量的 .then() 方法,导致语义不够清晰,难以直观地反映代码的执行流程。

以下是一个使用 fetch 发起连续网络请求的例子,fetch 是浏览器原生支持的 API,它返回一个 Promise 对象。

fetch('https://www.geekbang.org')
.then((response) => {
    console.log(response)
    return fetch('https://www.geekbang.org/test')
})
.then((response) => {
    console.log(response)
})
.catch((error) => {
    console.log(error)
})

尽管 Promise 让异步流程线性化了,但 .then 的链式调用仍然让代码显得繁琐,可读性有待提高。因此,ES7 引入了 async/await,这是 JavaScript 异步编程的一次重大革新。它允许我们以同步代码的风格编写异步逻辑,在不阻塞主线程的前提下访问资源,极大提升了代码的清晰度。

async function foo(){
    try{
        let response1 = await fetch('https://www.geekbang.org')
        console.log('response1')
        console.log(response1)
        let response2 = await fetch('https://www.geekbang.org/test')
        console.log('response2')
        console.log(response2)
    }catch(err) {
        console.error(err)
    }
}
foo()

通过这段代码,你会发现整个异步操作被封装在了同步的代码结构中,并且能够使用 try...catch 进行异常捕获。这完全符合人类的线性思维习惯。不过,要真正理解这种“同步风格异步代码”背后的魔法,我们需要先了解两个底层概念:生成器(Generator)与协程(Coroutine)。async/await 正是结合了 Generator 和 Promise 两种技术的语法糖。

生成器(Generator)与协程(Coroutine)原理

生成器函数是一种可以暂停执行和恢复执行的特殊函数。 在定义时,需要在 function 关键字后添加一个星号 *

function* genDemo() {
    console.log("开始执行第一段")
    yield 'generator 2'
    console.log("开始执行第二段")
    yield 'generator 2'
    console.log("开始执行第三段")
    yield 'generator 2'
    console.log("执行结束")
    return 'generator 2'
}
console.log('main 0')
let gen = genDemo()
console.log(gen.next().value)
console.log('main 1')
console.log(gen.next().value)
console.log('main 2')
console.log(gen.next().value)
console.log('main 3')
console.log(gen.next().value)
console.log('main 4')

执行上面代码,你会发现 genDemo 函数并非一次性执行完毕,而是与全局代码交替执行。这正是生成器的核心特性:

  1. 在生成器函数内部执行时,遇到 yield 关键字,引擎会暂停当前函数执行,并将 yield 后面的值返回给外部。
  2. 外部可以通过调用生成器对象的 .next() 方法,恢复该函数的执行。

那么,函数的“暂停”与“恢复”是如何实现的呢?这就要引出协程的概念。

协程是一种比线程更加轻量级的执行单元。你可以将它理解为跑在线程上的任务。一个线程上可以同时存在多个协程,但在任意时刻,只能执行其中一个。如果当前正在执行协程 A,要启动协程 B,就需要 A 暂停并将线程控制权交给 B;反之亦然。通常,我们称启动方为父协程。

协程完全由程序控制(在用户态执行),而非操作系统内核管理。这避免了线程切换的开销,性能优势显著。在前端框架/工程化开发中,理解这些底层机制对性能优化很有帮助。

JavaScript生成器流程图

从上图及代码执行中,我们可以总结出协程的几点关键规则:

  1. 调用生成器函数 genDemo 会创建一个协程 gen,但创建后它不会立即执行。
  2. 必须通过调用 gen.next() 来启动或恢复 gen 协程的执行。
  3. 协程执行中遇到 yield 会暂停,并将信息返回给父协程。
  4. 协程执行中遇到 return,JavaScript 引擎会结束当前协程,并将值返回给父协程。

特别需要注意的是:

  • gen 协程与父协程是交替执行的,并非并发。
  • yieldgen.next() 被调用时,JavaScript 引擎会保存当前协程的调用栈信息,并恢复另一个协程的调用栈信息,从而实现执行上下文的切换。

JavaScript协程调用流程图

实际上,生成器就是 JavaScript 中协程的一种实现方式。现在,我们可以用生成器配合 Promise 来改写最初的异步请求示例。

//foo函数
function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}
//执行foo函数的代码
let gen = foo()
function getGenPromise(gen) {
    return gen.next().value
}
getGenPromise(gen).then((response) => {
    console.log('response1')
    console.log(response)
    return getGenPromise(gen)
}).then((response) => {
    console.log('response2')
    console.log(response)
})

这段代码的工作流程如下:

  1. 执行 let gen = foo(),创建生成器协程 gen
  2. 父协程执行 gen.next(),将控制权交给 gen 协程。
  3. gen 协程执行 fetch 创建 Promise 对象 response1,然后通过 yield 暂停自身,并将 response1 返回给父协程。
  4. 父协程恢复,调用 response1.then() 注册回调,等待请求完成。
  5. 请求完成后,then 的回调被触发,在其中再次调用 gen.next(),将控制权和结果交还给 gen 协程,继续执行下一个请求。

通常,我们会将执行生成器的逻辑封装成一个函数,称为执行器(如著名的 co 库),以简化调用。

function* foo() {
    let response1 = yield fetch('https://www.geekbang.org')
    console.log('response1')
    console.log(response1)
    let response2 = yield fetch('https://www.geekbang.org/test')
    console.log('response2')
    console.log(response2)
}
co(foo());

深入解析 async/await 机制

虽然“生成器 + Promise + 执行器”的模式已经可行,但为了追求更简洁直观的代码,ES7 正式推出了 async/await 语法。它的底层仍然是 Promise 和生成器(协程),可以看作是对这两者的高级封装。

1. async 函数

根据 MDN 定义,async 函数是一个通过异步执行并隐式返回 Promise 作为结果的函数。

async function foo() {
    return 2
}
console.log(foo()) // 输出: Promise {<fulfilled>: 2}

2. await 表达式

await 的行为是理解 async/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

让我们站在协程的视角,结合下图来梳理整个执行流程:

JavaScript Promise流程图

  1. 首先打印 0
  2. 执行 foo()。由于 foo 被标记为 async,引擎会保存当前调用栈信息。接着执行 foo 内部的 console.log(1),打印 1
  3. 执行 await 100。这一步隐含了以下操作:
    • 引擎会默认创建一个 Promise 对象let promise_ = new Promise((resolve) => resolve(100)))。
    • 创建 Promise 时,resolve(100) 的任务会被提交到微任务队列
    • foo 协程暂停执行,控制权交还给父协程,同时将 promise_ 对象返回。
  4. 父协程获得控制权后,会调用 promise_.then() 来监听 Promise 状态变化。
  5. 父协程继续执行,打印 3。父协程执行完毕后,进入微任务检查点,开始执行微任务队列。
  6. 微任务队列中的 resolve(100) 任务被执行,这会触发 promise_.then() 中注册的回调。这个回调被激活后:
    • 将主线程控制权交还给 foo 协程。
    • value(即 100)传递给协程。
  7. foo 协程恢复执行,将得到的 100 赋值给变量 a。接着执行后续代码,打印 1002。最后,控制权归还给父协程。

总结与思考

Promise 虽然解决了回调地狱,但其链式调用的 .then() 方法在语义表达上仍有不足。async/await 的出现,使得我们可以用近乎同步的代码风格编写异步逻辑。这背后依赖于生成器(协程)的暂停/恢复能力以及 Promise 的状态管理。

async/await 已成为现代 JavaScript 异步编程的主流风格。实际上,不止 JavaScript,许多其他主流语言如 Python、Dart、C# 等都引入了类似的语法,这不仅让代码更整洁,也确保了函数的异步特性(始终返回 Promise)。

最后,我们通过一道经典的面试题来巩固对事件循环、微任务以及 async/await 执行顺序的理解:

async function foo() {
    console.log('foo')
}
async function bar() {
    console.log('bar start')
    await foo()
    console.log('bar end')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout')
}, 0)
bar();
new Promise(function (resolve) {
    console.log('promise executor')
    resolve();
}).then(function () {
    console.log('promise then')
})
console.log('script end')
// 输出顺序:
// script start
// bar start
// foo
// promise executor
// script end
// bar end
// promise then
// setTimeout



上一篇:vLLM-Omni 架构解析:如何实现文本、图像、音频的统一推理?
下一篇:深入剖析MySQL事务隔离级别:并发控制与ACID特性详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:36 , Processed in 0.210585 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表