为什么需要它
事件循环解释了 setTimeout、Promise.then、await、渲染之间的先后顺序。不先建立这个模型,异步代码只能靠背输出。
最小例子
js先把同步代码跑完
JavaScript 主线程只有一个调用栈。同步代码按顺序入栈、执行、出栈,在调用栈清空之前,事件循环不会去碰任务队列。
所以 console.log("D") 一定在 setTimeout 和 Promise 的回调之前执行。setTimeout(..., 0) 的 0 不是“立刻执行”,只是“尽快排进宏任务队列”。
- 调用栈:当前正在执行的函数
- 宏任务队列:setTimeout、setInterval、I/O、用户事件
- 微任务队列:Promise.then、queueMicrotask、MutationObserver
宏任务与微任务
宏任务和微任务不是两个同时被轮询的队列。事件循环每取出一个宏任务执行完,都会把当前微任务队列清空;清空之后才进入下一个宏任务。
这解释了一个常见现象:一个 Promise.then 里再产生 Promise.then,也会在当前这一轮全部执行完,不会排到下一个 setTimeout 后面。
微任务会在当前轮清空
js一次事件循环的顺序
可以把一轮事件循环记成一条固定流水线:执行一个宏任务 → 清空微任务 → 浏览器有机会渲染 → 取下一个宏任务。
注意“有机会渲染”不等于“每轮一定渲染”。浏览器会按刷新率合并渲染;如果微任务一直产生新的微任务,页面就会被卡住。
- 执行一个宏任务
- 清空当前所有微任务
- 浏览器决定是否渲染(样式、布局、绘制)
- 进入下一轮,取下一个宏任务
await 到底做了什么
await 不是把后面的代码“放到队列最后”,而是把 async 函数剩余部分挂起,等 Promise 落定后以微任务继续。
所以 await 后面的代码通常会在当前宏任务结束前、下一个 setTimeout 之前执行;但它的执行顺序仍然受微任务队列约束。
await 后面的代码是微任务
js和页面渲染的关系
如果微任务队列一直为空不出来,浏览器就没有机会进入渲染阶段。这就是“微任务饿死渲染”的风险,也是为什么不要在主线程里写超长同步循环。
requestAnimationFrame 的回调发生在渲染之前,适合做动画;setTimeout 只保证“尽快”,不保证帧对齐。