异步编程深潜:事件循环、Promise 与 async/await 的底层真相

张开发
2026/4/5 1:37:11 15 分钟阅读

分享文章

异步编程深潜:事件循环、Promise 与 async/await 的底层真相
JavaScript 是单线程的语言却能够高效地处理网络请求、用户交互、定时器等“耗时”操作这背后全靠一套精妙的异步编程模型。很多人会用 Promise 和 async/await但未必清楚事件循环里微任务和宏任务如何调度更不知道 async 函数不过是 Generator 的语法糖。今天我们就来深潜异步编程从底层机制到高层抽象彻底搞懂它。一、单线程的困境与回调JavaScript 设计之初是为了操作 DOM、响应用户事件如果采用多线程同时修改 DOM 会带来复杂的同步问题。因此它选择单线程 异步非阻塞模型。// 同步阻塞假设 sleep 是阻塞函数console.log(开始);sleep(3000);// 假想线程卡住3秒console.log(结束);// 3秒后才输出如果所有操作都同步执行网络请求或定时器就会让页面“卡死”。解决方案是回调把后续逻辑封装成函数等异步操作完成后调用。console.log(开始);setTimeout((){console.log(定时器完成);},3000);console.log(结束);// 输出开始 → 结束 → (3秒后) 定时器完成回调解决了阻塞问题但带来了“回调地狱”多层嵌套、错误处理混乱、难以追踪。于是 Promise 应运而生。二、事件循环微任务与宏任务要理解 Promise 和 async/await必须先掌握 JavaScript 的运行时模型——事件循环Event Loop。核心组件调用栈执行同步代码的栈结构函数调用时入栈返回时出栈。宏任务队列存放宏任务macro-task如setTimeout、setInterval、I/O、UI 渲染。微任务队列存放微任务micro-task如Promise.then、MutationObserver、queueMicrotask。循环规则执行一个宏任务从队列头部取出。执行过程中产生的微任务全部依次执行清空微任务队列。必要时进行 UI 渲染浏览器约 16.6ms 一次。回到第 1 步取出下一个宏任务。关键点微任务会在当前宏任务结束、下一个宏任务开始之前全部执行。console.log(1);setTimeout((){console.log(2);Promise.resolve().then(()console.log(3));},0);Promise.resolve().then((){console.log(4);setTimeout(()console.log(5),0);});console.log(6);// 输出顺序1, 6, 4, 2, 3, 5分析同步代码1、6 直接输出。微任务队列Promise.then 输出 4内部又添加了宏任务 5。当前宏任务结束清空微任务 → 输出 4 结束。下一个宏任务setTimeout 回调输出 2然后它的微任务输出 3。再下一个宏任务输出 5。理解这个顺序是调试异步 bug 的基础。三、Promise从零实现一个简版Promise 是一个状态机有三种状态pending、fulfilled、rejected。状态一旦改变就不可逆。我们手写一个简化版就能看清 then 的回调是如何注册和调用的。classMyPromise{constructor(executor){this.statepending;this.valueundefined;this.callbacks[];// 存储 then 注册的回调constresolve(value){if(this.state!pending)return;this.statefulfilled;this.valuevalue;this.callbacks.forEach(cbthis._handle(cb));};constreject(reason){/* 类似实现 */};try{executor(resolve,reject);}catch(e){reject(e);}}then(onFulfilled,onRejected){returnnewMyPromise((resolve,reject){this.callbacks.push({onFulfilled:(){try{constresultonFulfilled(this.value);resolve(result);}catch(e){reject(e);}},onRejected:(){/* 类似处理 */}});});}_handle(callback){if(this.statefulfilled){callback.onFulfilled();}elseif(this.statepending){this.callbacks.push(callback);}}}真正的 Promise 规范还涉及值的穿透、多次 then 链式调用、以及微任务调度上面简化版是同步调用回调实际需要用queueMicrotask或setTimeout包装。但核心思想不变then 只是注册回调resolve/reject 触发执行。四、async/awaitGenerator 的语法糖很多人觉得 async 函数很神奇函数内部可以写同步风格的代码却能等待异步结果。实际上它的底层是Generator 自动执行器。Generator 基础Generator 函数可以暂停和恢复执行function*gen(){constayield1;console.log(a);constbyield2;returnb;}constitgen();console.log(it.next());// { value: 1, done: false }console.log(it.next(A));// 输出 A, { value: 2, done: false }console.log(it.next(B));// { value: B, done: true }yield可以返回一个值并等待next传参进来恢复执行。这个特性恰好可以用来模拟“等待异步结果”。模拟 async/await假如我们想让 generator 能自动执行并支持 Promisefunctionrun(generatorFunc){constitgeneratorFunc();functionstep(prevResult){const{value,done}it.next(prevResult);if(done)returnPromise.resolve(value);// 如果 value 是 Promise等它完成后再继续returnPromise.resolve(value).then(step);}returnstep();}// 使用run(function*(){constdata1yieldfetch(/api/1).then(rr.json());constdata2yieldfetch(/api/2).then(rr.json());console.log(data1,data2);});这个run函数就是一个自动执行器。而async/await正是这种模式的语法糖async函数返回 Promiseawait后面跟 Promise引擎自动在then中恢复执行。// 用 async/await 等价写法asyncfunctionfetchData(){constdata1awaitfetch(/api/1).then(rr.json());constdata2awaitfetch(/api/2).then(rr.json());console.log(data1,data2);}所以await的本质就是把后续代码包装成一个微任务回调挂载到 Promise 上。五、常见陷阱与最佳实践1. 忘记 return 导致并行变串行// 错误顺序执行总耗时 请求1 请求2asyncfunctionbad(){constaawaitfetch(/api/a);constbawaitfetch(/api/b);// 等 a 完成才发 b}// 正确并发执行asyncfunctiongood(){constp1fetch(/api/a);constp2fetch(/api/b);const[a,b]awaitPromise.all([p1,p2]);}2. 不要在循环内使用 await除非故意串行需要并发时用Promise.all或Promise.allSettled。3. 未捕获的 Promise 拒绝// 危险错误被吞掉asyncfunctiondanger(){thrownewError(Oops);}danger();// 无报错但产生一个未处理的拒绝// 正确要么 await要么 .catchawaitdanger();// 或 danger().catch(console.error);现代 Node.js 和浏览器会在未捕获的 Promise 拒绝时打印警告但最好显式处理。4. 微任务过多导致 UI 阻塞// 递归 Promise.then 会一直占用微任务队列导致无法执行宏任务包括 UI 渲染functionloop(){Promise.resolve().then(loop);}loop();解决方案使用setTimeout或queueMicrotask控制。六、总结异步编程深潜我们看到事件循环是调度核心宏任务和微任务的执行顺序决定了代码输出。Promise本质是状态机then 注册回调resolve 触发微任务执行。async/await是 Generator 的语法糖让异步代码像同步一样书写。性能与正确性需要理解并发、错误处理和微任务队列。掌握这些底层机制你不仅能写出更可靠的异步代码还能轻松调试那些“为什么先输出 2 后输出 3”的谜题。下次面试官问“事件循环”你可以自信地从宏任务、微任务讲到 async/await 的实现原理。异步编程不再黑盒。立即进入

更多文章