敲黑板!async/await应用原理

async/await继发运行

先看一个常见的场景:通过 for循环 配合 async/await,可以实现按顺序执行异步操作。

javascript 复制代码
function syncTime(delay, res) {
    return new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(res);
        }, delay)
    })
}

async function main() {
    for(let i=0 ;i<10; i++){
        let res = await syncTime(1000, i)
        console.log(`%c ${res}`, `color: red;`);
    }
}
main();

运行结果:会依次打印 0...9,每停顿1秒打印一次。如果你把 syncTime 函数中的 setTimeout 替换为真实的异步请求,那么它就可以直接用到项目中。

async/await 只是 Promise 的升级版嘛?看到上面的代码,或者已经知道这个特性的同学,肯定会回答不是。应该说,它算是 Promise + Generator 的结合体。

async/await 实现原理

以上文的代码为示例,我们来看看 for + await 执行时发生了什么?

每次循环都会触发下列过程:调用 syncTime 函数,return 出一个 Promise 实例,在 delay ms 后 Promise 实例状态由 pending 变为 resolved,代码继续运行,console 再打印数字。循环遍历 10 次。

等等!不是应该先直接打印 console,然后才运行 setTimeout?JS 的 Event Loop 怎么在这里失效了?下面让我们深入看看。

async/await 和生成器

在说 async/await 之前,先说下 Generator(生成器),它是一种可以暂停和恢复执行的函数。

lua 复制代码
function* generatorDemo() {
    console.log('start');
    yield 1;
    console.log('middle');
    yield 2;
    console.log('end');
}

const gen = generatorDemo();
gen.next(); // start, { value: 1, done: false }
gen.next(); // middle, { value: 2, done: false }
gen.next(); // end, { value: undefined, done: true }

关键点:yield 会暂停函数执行,并等待外部调用 next() 恢复。这就像一个协作任务的开关。

自动执行器

光有 Generator 还不够,我们需要一个自动执行器来驱动它不断调用 next(),直到结束。这个自动执行器就是 async/await 的前身。

ini 复制代码
function runGenerator(gen) {
    const generator = gen();
    function step(nextValue) {
        const result = generator.next(nextValue);
        if (result.done) return result.value;
        result.value.then(val => step(val));
    }
    step();
}

这个 runGenerator 函数做的事情:

  1. 创建生成器实例
  2. 调用 next() 获取结果
  3. 如果 donetrue,结束
  4. 如果 donefalse,把 value(通常是个 Promise)用 .then() 包裹,resolve 后继续调用 step

async/await 语法糖

async/await 实际上就是上面自动执行器的语法简化。看下面的对比:

javascript 复制代码
// Generator 版本
function* fetchData() {
    const data1 = yield fetch('/api/user');
    const data2 = yield fetch('/api/posts');
    return data2;
}

// async/await 版本
async function fetchData() {
    const data1 = await fetch('/api/user');
    const data2 = await fetch('/api/posts');
    return data2;
}

async 关键字让函数返回一个 Promise,await 关键字则替代了 yield,自动处理 Promise 的 resolve 和 reject。

编译后的真相

async/await 到底做了什么?Babel 编译后会揭示答案:

javascript 复制代码
// 原始代码
async function main() {
    const res = await syncTime(1000, 1);
    console.log(res);
}

// 编译后(简化版)
function main() {
    return _asyncToGenerator(function* () {
        const res = yield syncTime(1000, 1);
        console.log(res);
    })();
}

_asyncToGenerator 就是那个自动执行器,它负责驱动 Generator 运行,直到 Promise 完成或结束。

状态切换

async 函数在执行时,会维护一个状态机:

javascript 复制代码
等待中 → Promise resolve → 继续执行 → 等待中 → ...

每次遇到 await,就会暂停函数执行,等待后面的 Promise resolve 后再继续。暂停期间,调用栈是空的,JavaScript 可以去执行其他任务。

继发与并发

回到文章开头,for + await 为什么是继发执行?

javascript 复制代码
async function main() {
    for(let i=0 ;i<10; i++){
        let res = await syncTime(1000, i)
        console.log(res);
    }
}

因为每次循环都在 await 处暂停,必须等当前 Promise resolve 后,才会执行下一次循环的 await。这就是继发(串行)。

如果想要并发执行,应该这样:

javascript 复制代码
async function main() {
    const promises = [1,2,3,4,5].map(i => syncTime(1000, i));
    const results = await Promise.all(promises);
    console.log(results); // 一次性等所有 Promise 完成
}

Promise.all 会同时启动所有异步任务,等全部完成后一起返回结果。这就是并发(并行)。

Promise 执行流程

关于 Promise 的详细解析,可以参考另一篇文章:

Promise原理解析

Promise 的 .then()async/await 本质上都是注册回调,等待 Promise 状态变更后执行。async/await 只是让写法更同步化。

async 函数返回值

async 函数必然返回一个 Promise:

javascript 复制代码
async function demo() {
    return 123;
}
console.log(demo() instanceof Promise); // true

// 等价于
function demo() {
    return Promise.resolve(123);
}

如果 async 函数内部抛出异常,则会返回一个 rejected 的 Promise:

javascript 复制代码
async function demo() {
    throw new Error('Oops!');
}
demo().catch(err => console.log(err.message)); // Oops!

错误处理

async/await 的错误处理用 try...catch

javascript 复制代码
async function main() {
    try {
        const res = await fetch('/api/data');
        const data = await res.json();
        console.log(data);
    } catch (err) {
        console.error('请求失败:', err);
    }
}

这比 Promise 的 .catch() 更直观,代码阅读起来和同步写法几乎一致。

捕获多个 await

javascript 复制代码
async function main() {
    try {
        const user = await fetchUser();
        const posts = await fetchPosts(user.id);
        const comments = await fetchComments(posts[0].id);
    } catch (err) {
        // 三个 await 中任意一个 reject,都会进入这里
        console.error('流程异常:', err);
    }
}

async/await 踩坑

1. 平行await vs 继发await

scss 复制代码
// 继发:每个 await 等待上一个完成
const a = await fetchA();    // 等待 1s
const b = await fetchB(a);   // 等待 1s
const c = await fetchC(b);   // 等待 1s
// 总耗时:3s

// 并发:Promise.all 同时发起
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// 总耗时:1s

2. await 在 forEach 中陷阱

scss 复制代码
// 错误写法
[1, 2, 3].forEach(async (item) => {
    await fetch(item); // forEach 不会等待 async 回调
});

// 正确写法:使用 for...of
for (const item of [1, 2, 3]) {
    await fetch(item);
}

3. async 回调函数

javascript 复制代码
// 错误
arr.map(async (item) => {
    return await fetch(item);
}); // 返回的是 Promise[],不是数据[]

// 正确
const results = await Promise.all(arr.map(item => fetch(item)));

参考

ES6中的Iterator迭代器

Promise原理解析

MDN: async function

ECMAScript: async function definitions


著作权归Herrylo所有 原文链接:herrylo.github.io/frontend/20...

相关推荐
DevUI团队31 分钟前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
weixin_431600441 小时前
NestJS 入门(3):Guard 如何挡住未登录请求?
前端·后端·学习·nest.js
avi91112 小时前
[AI教做人]AI平台做2项目;一个3D模型展示,另一个框架多人
javascript·人工智能·ai·3d模型·3d引擎·顶点和法线
kyriewen2 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
IT_陈寒2 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端
用户938515635072 小时前
Type vs Interface:读完这篇就没有面试官能难倒你了
前端·面试·typescript
用户938515635073 小时前
手写一个 LLM Harness 框架:用工程化手段把大模型幻觉踩在脚下
javascript·人工智能·后端
油丶酸萝卜别吃3 小时前
jquery-ajax.js 说明文档
前端·javascript·jquery
windliang3 小时前
Claude Code 源码分析(九):子 Agent 如何分叉、继续与回到父会话
前端·javascript·面试
柒和远方4 小时前
V063: TS 面试必考:interface 与 type 的四大差异,与 LLM Harness 的自动化择优
前端·javascript