-
Notifications
You must be signed in to change notification settings - Fork 21
敲黑板!async/await应用原理 #52
Description
敲黑板!async/await应用和原理
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 函数做的事情:
- 创建生成器实例
- 调用
next()获取结果 - 如果
done为true,结束 - 如果
done为false,把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 的 .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)));
参考
ECMAScript: async function definitions