Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

敲黑板!async/await应用原理 #52

Open
Labels

Description

敲黑板!async/await应用和原理

作者:HerryLo
博客原文链接

async/await继发运行

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

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(生成器),它是一种可以暂停和恢复执行的函数。

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 的前身。

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 实际上就是上面自动执行器的语法简化。看下面的对比:

// 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 编译后会揭示答案:

// 原始代码
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 函数在执行时,会维护一个状态机:

等待中 → Promise resolve → 继续执行 → 等待中 → ...

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

继发与并发

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

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

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

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

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:

async function demo() {
 return 123;
}
console.log(demo() instanceof Promise); // true
// 等价于
function demo() {
 return Promise.resolve(123);
}

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

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

错误处理

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

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

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

// 继发:每个 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 中陷阱

// 错误写法
[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 回调函数

// 错误
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


Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /