JS 异步:Event-Loop+async/await 前言在前端开发中是不是经常被JS的异步代码绕晕明明写的代码顺序一样运行结果却大相径庭其实核心原因就在于——JS是单线程语言而异步操作全靠「Event-Loop事件循环」来调度。今天就结合具体代码案例从进程线程、V8引擎到Event-Loop、async/await一步步拆解让你搞懂JS异步的底层逻辑~一、先搞基础进程 vs 线程在聊JS异步之前我们得先分清两个容易混淆的概念进程和线程这是理解后续内容的基石进程简单来说进程就是CPU运行指令、加载和保存上下文所需的“容器”是程序运行的独立单位。比如你打开浏览器每多开一个Tab页就相当于多开启了一个进程每个进程之间相互独立互不干扰。线程线程是进程内的执行单元是CPU实际执行指令的“最小单位”。一个进程可以包含多个线程这些线程共享进程的资源协同完成任务。举个浏览器的例子每个浏览器Tab进程中会包含多个核心线程其中和我们JS相关的有3个渲染线程负责渲染页面HTML、CSS渲染JS引擎线程负责执行JS代码HTTP请求线程负责发送网络请求。这里有个关键知识点⚠️因为JS代码可以修改DOM比如document.write、appendChild如果JS引擎线程和渲染线程同时运行会导致页面渲染混乱所以JS引擎线程和渲染线程是互斥的——也就是说JS代码执行时渲染线程会暂停等JS执行完渲染线程才会继续工作。这也是为什么有时候JS代码写得太复杂页面会出现“卡顿”的原因二、V8引擎JS单线程的“幕后推手”我们写的JS代码最终是由V8引擎来执行的。而V8引擎在执行JS代码时默认只开启一个JS引擎线程——这就意味着JS代码只能“自上而下、依次执行”同一时间只能做一件事。那问题来了如果JS遇到耗时操作比如setTimeout、网络请求、读取文件难道要一直等着操作完成再继续执行后续代码吗这样会导致页面卡死用户体验直接拉胯为了解决这个问题JS引入了「异步机制」单线程处理代码时遇到同步任务就立即执行遇到异步任务不等待、不阻塞而是把它暂时存放到“任务队列”中等JS引擎线程空闲时再去执行任务队列中的异步任务。三、核心重点Event-Loop 事件循环Event-Loop事件循环就是JS处理异步任务的“调度器”它的执行流程决定了所有同步、异步代码的运行顺序。我们先明确两个核心概念微任务和宏任务——所有异步任务都会被分到这两个队列中。3.1 微任务 vs 宏任务微任务和宏任务的区别在于它们的“优先级”微任务优先级高于宏任务会先于宏任务执行。微任务优先级高Promise.then()、Promise.catch()、Promise.finally()process.nextTick()Node.js环境浏览器不支持MutationObserver监听DOM变化的API宏任务优先级低整个script脚本最外层的同步代码属于宏任务的开端setTimeout()、setInterval()AJAX请求、I/O操作比如读取文件UI渲染页面渲染操作易错点提醒⚠️很多人会误以为“setTimeout(fn, 0)”会立即执行其实不然——setTimeout的延迟时间是“最小延迟”不是“精确延迟”即使设为0也会被放入宏任务队列等待同步代码、微任务全部执行完毕后才会执行。3.2 Event-Loop 执行顺序记住这个顺序就能搞定80%的异步代码输出题结合后面的代码案例理解更透彻先执行同步代码最外层script脚本属于宏任务执行过程中遇到异步任务就分别存入微任务队列、宏任务队列同步代码执行完毕后清空微任务队列所有微任务依次执行执行过程中产生的新微任务也会在本次微任务队列中执行完毕微任务全部执行结束后若有需要如DOM发生变化浏览器会进行页面渲染渲染完成后从宏任务队列中取出第一个宏任务执行执行该宏任务的过程中遇到同步、异步任务重复步骤1-2重复步骤1-4形成“循环”这就是Event-Loop。3.3 代码实操搞懂Event-Loop执行顺序结合你给出的第一段代码我们一步步拆解执行过程看看为什么输出结果是「1 2 7 3 5 4 6」console.log(1); // 同步代码输出1 new Promise((resolve) { console.log(2); // Promise构造函数内是同步代码输出2 resolve() }) .then(() { console.log(3); // 微任务存入微任务队列 setTimeout(() { console.log(4); // 宏任务存入宏任务队列延迟0ms }, 0) }) setTimeout(() { console.log(5); // 宏任务存入宏任务队列延迟0ms setTimeout(() { console.log(6); // 宏任务存入宏任务队列延迟0ms }, 0) }, 0) console.log(7); // 同步代码输出7执行步骤拆解console.log(1)同步输出「1」new Promise构造函数内是同步代码console.log(2)输出「2」调用resolve()将then回调存入微任务队列记为微1遇到setTimeout延迟0ms宏任务存入宏任务队列记为宏1console.log(7)同步输出「7」同步代码执行完毕开始清空微任务队列执行微1then回调console.log(3)输出「3」遇到setTimeout延迟0ms宏任务存入宏任务队列记为宏2微任务队列清空渲染页面本次无明显渲染执行宏任务队列第一个宏任务宏2console.log(5)输出「5」遇到setTimeout延迟0ms宏任务存入宏任务队列记为宏3宏2执行完毕再次检查微任务队列无新微任务执行下一个宏任务宏2console.log(4)输出「4」宏3执行完毕检查微任务队列无执行下一个宏任务宏3console.log(6)输出「6」。所以最终输出顺序就是1 2 7 3 5 4 6 ✅3.4 再练一题巩固Event-Loop再看这段代码console.log(1); setTimeout(() { console.log(2); setTimeout(() { console.log(3) }, 1000) }, 0) setTimeout(() { console.log(4) }, 2000) console.log(5);执行步骤拆解执行同步代码console.log(1)→ 输出1遇到第一个setTimeout(..., 0)延迟 0ms 后把回调输出 2 嵌套定时器推入宏任务队列遇到第二个setTimeout(..., 2000)延迟 2000ms 后把回调输出 4推入宏任务队列执行同步代码console.log(5)→ 输出5同步代码执行完毕开始处理宏任务队列取出第一个宏任务执行 → 输出2执行中遇到嵌套的setTimeout(..., 1000)延迟 1000ms 后把回调输出 3推入宏任务队列6.此时宏任务队列里只有「延迟 2000ms 的输出 4」在等待7.时间流逝1000ms 到输出3被推入宏任务队列 → 立刻执行 → 输出3再等 1000ms总计 2000ms输出4被推入宏任务队列 → 执行 → 输出4。最终输出顺序1 → 5 → 2 → 3 → 4。宏任务队列是先进先出为什么 3 比 4 先输出关键点两个定时器不是同时入队输出 4 的定时器一开始就设定了 2000ms 延迟2000ms 后才入队输出 3 的定时器等第一个宏任务执行完瞬间完成才设定 1000ms 延迟1000ms 后就入队执行。1000ms 2000ms所以3必然比4先执行和宏任务队列顺序无关。JavaScript 中setTimeout的延迟时间是回调函数加入宏任务队列的等待时间而非执行时间宏任务队列遵循先进先出但不同定时器的回调不是同时入队谁的延迟时间先耗尽谁就先入队先执行。四、进阶async/await 异步语法糖async/await 是ES7引入的异步语法本质是Promise的“语法糖”让异步代码写起来更像同步代码可读性大大提升。4.1 async/await 核心规则async关键字函数前面加async等同于函数内部自动返回一个Promise实例对象。比如async function fn() { return 1; // 等同于 return Promise.resolve(1); } fn().then(res console.log(res)); // 输出1await关键字必须跟async配合使用不能单独使用如果await后面接的不是Promise对象await就无法“约束”它会直接执行后续代码如果await后面接Promise对象会“暂停”当前async函数的执行等待Promise状态变为resolved成功或rejected失败再继续执行后续代码。关键原理await fn() 之所以能“当成同步看待”核心是——await会把它后续的代码(当前async函数内await后面的所有代码挤到微任务队列中等await后面的Promise执行完成后再执行这个微任务。4.2 代码实操async/await 执行顺序先看这段基础代码理解async/await和Promise的关联function a() { return new Promise((resolve, reject) { setTimeout(() { console.log(a); // 宏任务 resolve() }, 1000) }) } function b() { console.log(b); // 同步 } // 案例1Promise.then写法 a().then(() { b() }) console.log(hello); // 同步输出顺序hello → a → b同步代码先执行a()是Promisethen回调是微任务等待a()的宏任务执行完再执行微任务b()再看async/await写法对比差异function a() { return new Promise((resolve, reject) { setTimeout(() { console.log(a); // 宏任务 resolve() }, 1000) }) } function b() { console.log(b); // 同步 } async function foo() { setTimeout(() { console.log(c); // 宏任务延迟1500ms }, 1500) await a() // 等待a()的Promise resolve后续代码进入微任务 b() console.log(hello); } foo()执行步骤拆解调用foo()执行async函数内部代码遇到setTimeout延迟1500ms宏任务存入宏任务队列记为宏A遇到await a()a()返回Promise里面有setTimeout延迟1000ms宏任务记为宏B此时foo()暂停执行等待宏B执行完毕、Promise resolve同步代码执行完毕此时foo()暂停无其他同步代码检查微任务队列无执行宏任务队列先执行宏B延迟1000msconsole.log(a)输出「a」调用resolve()此时await等待结束将foo()后续的代码b()、console.log(hello)存入微任务队列宏B执行完毕检查微任务队列执行微任务b()输出「b」console.log(hello)输出「hello」微任务执行完毕执行下一个宏任务宏A延迟1500msconsole.log(c)输出「c」。最终输出顺序a → b → hello → c ✅4.3 综合案例async/await Promise setTimeoutconsole.log(script start); // 同步 async function async1() { await async2() // 等待async2()执行后续代码进入微任务 console.log(async1 end); // 微任务 } async function async2() { console.log(async2 end); // 同步async函数内await前的代码是同步 } async1() setTimeout(() { console.log(setTimeout); // 宏任务 }, 0) new Promise((resolve, reject) { console.log(promise); // 同步 resolve() }) .then(() { console.log(then1); // 微任务 }) .then(() { console.log(then2); // 微任务then1执行完后存入 }); console.log(script end); // 同步输出顺序script start → async2 end → promise → script end → async1 end → then1 → then2 → setTimeout 关键提醒async函数内await前面的代码是同步执行的await后面的代码会被放入微任务队列和Promise.then的微任务优先级相同按顺序执行。五、总结异步核心知识点梳理1. JS是单线程由V8引擎的JS引擎线程执行与渲染线程互斥2. 异步任务分为微任务优先级高和宏任务优先级低3. Event-Loop执行顺序同步代码 → 微任务队列 → 页面渲染 → 宏任务队列循环4. async/await是Promise语法糖await会将后续代码放入微任务队列等待Promise resolve后执行。