关于eventloop
一些参考:
测试eventloop地址:https://www.jsv9000.app
参考视频:【事件循环】【前端】事件原理讲解
浏览器的事件循环机制(EventLoop)
概述:
要真正理解事件循环,我们需要先了解浏览器的多进程架构:
- 浏览器主进程:负责界面显示、用户交互
- GPU进程:处理图形渲染
- 网络进程:处理网络请求
- 渲染进程(核心):每个标签页一个渲染进程,包含:
- 主线程:执行JS、解析HTML/CSS、布局、绘制(就是我们常说的JS线程)
- 合成线程:负责图层分割
- 光栅线程:将图层转换为像素
关键点:JS引擎(如V8)只是渲染进程的一部分,JS的”单线程”指的是主线程的单线程,浏览器整体是多线程的。
关于浏览器渲染机制的笔记,可以查看:浏览器渲染机制
JavaScript的单线程特性与异步机制
JS为什么是单线程的?
JavaScript被设计成单线程的,主要是为了避免DOM操作的复杂性。如果JavaScript是多线程的,那么当多个线程同时操作同一个DOM元素时,就会出现竞态条件(Race Condition),导致不可预测的结果。例如,一个线程要删除某个DOM元素,另一个线程要修改它,那么到底应该以哪个线程的操作为准呢?为了避免这种复杂性,JavaScript从诞生之初就被设计为单线程。
单线程带来的问题与解决方案
(1)阻塞问题
- 单线程意味着所有任务需按顺序执行,若某个任务耗时过长(如复杂计算或网络请求),会阻塞后续代码执行,导致页面卡顿
- 解决方案:
- 异步编程模型:通过回调函数、Promise、async/await 处理异步操作,避免阻塞主线程
1 | // 示例:使用 Promise 处理异步请求 |
- **Web Worker**:将耗时任务交给后台线程处理,不阻塞主线程
1 | // 主线程 |
(2)I/O密集型场景的优化
- 浏览器中的 JavaScript 主要处理 I/O 密集型任务(如网络请求、文件操作),单线程模型配合异步 I/O 机制(如事件循环)可高效处理这类场景。
- 事件循环(Event Loop):
- JavaScript 通过事件循环机制处理异步任务,将耗时操作放入任务队列,主线程空闲时再处理这些任务。
事件循环的完整运行机制:
核心组件详解
(1)调用栈(Call Stack)
- 本质:记录函数调用的数据结构(LIFO栈),先进后出,如下图所示:

- 特点:
- 合成线程:每次函数调用都会创建新的栈帧(包含参数、局部变量等)
- 栈溢出:当递归深度超过最大调用栈大小(Chrome约1万层)
1 | // 栈溢出示例 |
(2)堆内存(Heap)
- 存储引用类型(对象、数组等)的内存区域
- 与栈的区别:
- 栈:自动分配固定大小内存(基础类型、指针)
- 堆:动态分配内存,需要垃圾回收
(3)任务队列系统
| 队列类型 | 触发方式 | 优先级 |
|---|---|---|
| 微任务队列 | JS引擎直接管理 | 高 |
| 宏任务队列 | 由浏览器宿主环境管理 | 低 |
| 动画回调队列 | requestAnimationFrame | 特殊 |
| 空闲回调队列 | requestIdleCallback | 最低 |
完整事件循环流程
下图为主线程中事件循环的运行示例图:

宏任务【Macro-tasks】与微任务【micro-tasks】
宏任务大概包括:
- script(整体代码)
- setTimeout
- setInterval
- setImmediate
- I/O(例如网络请求 ajax、文件读写)
- UI render
- setImmediate (Node.js环境特有)
微任务大概包括:
- Promise.then()、Promise.catch()、Promise.finally()
- process.nextTick (Node.js环境特有)
- MutationObserver (h5新特性 用于监听DOM变化)
EventLoop的执行顺序详解:
- 执行同步代码: 当JavaScript代码开始执行时,会首先执行所有的同步代码。这些同步代码可以被看作是当前宏任务的一部分。在执行过程中,如果遇到异步任务(无论是宏任务还是微任务),就会将其对应的回调函数放入相应的任务队列中。
- 清空微任务队列: 当所有同步代码执行完毕后,Event Loop并不会立即去执行宏任务队列中的任务。它会优先检查并清空微任务队列。这意味着,所有在当前宏任务执行期间产生的微任务,都会在下一个宏任务开始之前被执行完毕。
- 页面渲染(可选): 在微任务队列清空之后,如果浏览器判断有必要进行页面渲染(比如DOM结构发生了变化,或者需要更新UI),它就会进行一次页面渲染。这一步是可选的,浏览器会根据实际情况决定是否进行渲染。
- 执行下一个宏任务: 页面渲染完成后,Event Loop会从宏任务队列中取出一个任务来执行。这个任务执行完毕后,又会重复步骤2,检查并清空微任务队列,然后再次进行页面渲染(如果需要),接着再从宏任务队列中取出下一个任务……如此循环往复,直到所有任务执行完毕。
这个过程可以概括为:一个宏任务执行完毕 -> 清空所有微任务 -> 页面渲染(如果需要) -> 执行下一个宏任务。这个循环会一直持续下去,直到所有任务都执行完毕。
代码实战:
async/await在EventLoop中的表现:
概述:async/await是js中用于处理异步操作的语法糖,基于promise+generator构建
- async函数
- async函数隐式返回一个Promise对象。如果函数返回一个值,该值会被包装为Promise(通过
Promise.resolve);如果函数抛出异常,则返回的Promise状态为rejected。
- async函数隐式返回一个Promise对象。如果函数返回一个值,该值会被包装为Promise(通过
1 | async function foo(){ |
- await表达式
await只能在async函数内部使用。await后面可以跟一个Promise对象,它会暂停async函数的执行,等待Promise的状态变为resolved,然后返回结果值。- 如果
await后面是一个非Promise的值,它会被立即转换为一个已解决的Promise。 - 如果Promise被拒绝(rejected),
await会抛出拒绝的原因(可以使用try/catch捕获)
async函数在事件循环中造成的细微差别:
重点:
根据规范,async函数返回一个Promise,并且await会暂停函数的执行,等待Promise解决,然后继续执行函数,并将后续代码放入微任务队列。
特别要注意的是:当async函数返回一个Promise时,会有额外的微任务产生(因为需要等待返回的Promise被解决,然后才能解决async函数自己的Promise)。
async async2()函数分析:
执行同步代码:
- 输出 'script start'
- 定义async1和async2(不执行)
- 调用async1()
- 在async1中,调用async2()(因为await后面是async2(),所以先执行async2)
- 执行async2:
- 输出 'async2 end'
- 然后执行 return Promise.resolve().then(...)
这里,Promise.resolve()返回一个已解决的Promise,然后调用then方法,将then的回调(输出'async2 end1')放入微任务队列。 - 注意:async2是一个async函数,它返回的Promise不是直接这个then返回的Promise,而是会额外包装一层。
根据ECMAScript规范,async函数内部return一个值x,相当于执行Promise.resolve(x),然后会等待x(如果x是Promise)解决,再解决async函数返回的Promise。但是,如果x是一个Promise,那么就会产生两个微任务:一个用于等待x解决,另一个用于解决async函数的Promise。
具体到async2:
async2中:return Promise.resolve().then(...)
这个then方法返回一个新的Promise(我们称为P1)。而async2函数会返回一个新的Promise(称为P2),P2的解决会等待P1的解决。所以,在P1解决后,才会解决P2,然后async1中的await才会继续。
- 继续同步代码:
- 调用setTimeout,将回调放入宏任务队列(0毫秒后,但会在当前宏任务执行完后执行)
- 执行new Promise,输出 'Promise',并立即resolve,将第一个then的回调(输出'promise1')放入微任务队列
- 输出 'script end'
此时,同步代码执行完毕。当前微任务队列中有两个任务(注意顺序): - 第一个是async2中放入的:输出'async2 end1'(来自P1的then回调)
- 第二个是new Promise的then回调:输出'promise1'
- 开始执行微任务队列:
第一个微任务:执行async2中then的回调
输出 'async2 end1'
这个回调执行完毕,P1被解决(值为undefined,因为没有return)
这时,因为async2返回的P2在等待P1解决,所以会安排一个微任务来解决P2(这是规范要求的,当async函数返回一个Promise时,需要等待这个Promise解决,然后才能解决async函数自己的Promise。这个等待过程会产生一个微任务)。
第二个微任务:执行输出'promise1'
输出 'promise1'
由于这个then回调返回undefined,所以它返回的Promise立即解决,于是下一个then的回调(输出'promise2')被放入微任务队列。
此时,微任务队列中新增了两个微任务(按顺序):
微任务3:解决async2返回的P2(这个微任务会触发async1中await后面的代码放入微任务队列)
微任务4:输出'promise2'
注意:微任务3是在执行第一个微任务(输出'async2 end1')后产生的,所以它排在微任务4(由第二个微任务产生)之前。
当前微任务队列:[微任务3, 微任务4] - 继续执行微任务队列:
执行微任务3:解决async2返回的P2
此时,await async2()的Promise(即P2)被解决,然后await后面的代码(输出'async1 end')被放入微任务队列。
执行微任务4:输出 'promise2'
此时,微任务队列中新增了一个微任务(输出'async1 end') - 继续执行微任务队列(新的一轮?不,微任务队列会一直执行直到清空,所以会继续):
微任务5:输出 'async1 end' - 微任务队列清空,执行宏任务:
输出 'setTimeout'
阶段1:同步代码执行(宏任务)
同右
微任务队列:[ 回调A (async2 end1), 回调B (promise1) ]
阶段2:微任务执行(第一轮)
- 执行回调A:
○ console.log(‘async2 end1’) → 输出 “async2 end1”
○ 解析 P2(async2 内部 Promise)
○ await async2() 完成,将 async1 end 加入微任务队列(回调C)
微任务队列更新:
[ 回调B (promise1), 回调C (async1 end) ] - 执行回调B:
○ console.log(‘promise1’) → 输出 “promise1”
○ 返回 undefined,自动创建新解析的 Promise P4
○ 将 promise2 回调加入微任务队列(回调C)
微任务队列更新:
[ 回调C (async1 end) , 回调D (promise2)]
阶段3:微任务执行(第二轮) - 执行回调C:
○ console.log(‘async1 end’) → 输出 “async1 end” - 执行回调D:
○ console.log(‘promise2’) → 输出 “promise2”
阶段4:宏任务执行 - 执行 setTimeout 回调:
○ console.log(‘setTimeout’) → 输出 “setTimeout”
阶段1:同步代码执行(宏任务)
- console.log(‘script start’)
→ 输出 “script start” - 定义函数:
○ 定义 async1 和 async2(不执行函数体) - 调用 async1():
○ 进入 async1,遇到 await async2()
○ 调用 async2() - 执行 async2():
○ console.log(‘async2 end’) → 输出 “async2 end”
○ return Promise.resolve().then(…)
创建 Promise P2 并将 .then 回调加入微任务队列(回调A) - setTimeout:
○ 将回调加入宏任务队列 - new Promise:
○ console.log(‘Promise’) → 输出 “Promise”
○ resolve() 立即解析 Promise P3
○ 将 .then 回调加入微任务队列(回调B) - console.log(‘script end’)
→ 输出 “script end”
微任务队列:[ 回调A (async2 end1), 回调B (promise1) ]
阶段2:微任务执行(第一轮) - 执行回调A:
○ console.log(‘async2 end1’) → 输出 “async2 end1”
○ 解析 P2(async2 内部 Promise)
○ 由于 async2 是 async 函数,需要额外微任务解析其返回的 Promise P1
微任务队列更新:
[ 回调B (promise1), 新任务 (解析P1) ] - 执行回调B:
○ console.log(‘promise1’) → 输出 “promise1”
○ 返回 undefined,自动创建新解析的 Promise P4
○ 将 promise2 回调加入微任务队列(回调C)
微任务队列更新:
[ 解析P1, 回调C (promise2) ]
阶段3:微任务执行(第二轮) - 执行解析P1任务:
○ 解析 async2 返回的 Promise P1
○ 使 await async2() 完成,将 async1 end 加入微任务队列(回调D)
微任务队列更新:
[ 回调C (promise2), 回调D (async1 end) ] - 执行回调C:
○ console.log(‘promise2’) → 输出 “promise2” - 执行回调D:
○ console.log(‘async1 end’) → 输出 “async1 end”
阶段4:宏任务执行 - 执行 setTimeout 回调:
○ console.log(‘setTimeout’) → 输出 “setTimeout”
await表达式在事件循环中,参考async和await的面试题, 举例:
1 | async function asy1(params) { |
分析如下:
执行流程:
- 调用
asy1(),进入asy1函数:
- 输出
1 - 调用
asy2()
- 在
asy2中:
- 调用
setTimeout,将回调函数放入宏任务队列(将在下一个宏任务执行)。 await后面是setTimeout返回的数字,所以会生成一个立即解决的Promise,并将asy2函数中await后面的代码(这里没有代码,所以实际上是等待这个Promise解决,然后asy2函数返回)包装成微任务,放入微任务队列。
- 继续执行同步代码:
- 输出
7 - 调用
asy3(),在asy3中,Promise.resolve().then将回调(输出6)放入微任务队列。
- 同步代码执行完毕,开始执行微任务队列:
- 微任务队列中有两个微任务:
- 由
asy2中的await产生的微任务(表示asy2函数可以继续执行,实际上就是让asy2函数返回,并解决asy1中await所等待的Promise)。 - 由
asy3放入的微任务(输出6)。
- 微任务队列按顺序执行:
- 首先执行第一个微任务(来自
asy2的await):这个微任务的执行会解决asy2函数返回的Promise,从而让asy1函数中await后面的代码(输出2)可以继续,但是注意,这个继续并不是立即执行输出2,而是将输出2的代码作为一个新的微任务加入微任务队列(因为async函数中,await后面的代码总是被包装成微任务)。 - 然后执行第二个微任务:输出
6。
- 此时微任务队列中又有了一个新的微任务(输出2),所以接下来执行这个微任务:输出
2。 - 微任务队列清空,然后执行宏任务队列中的定时器回调:
- 在定时器回调中:
- 执行
Promise.resolve().then,将输出3的回调放入微任务队列。 - 输出
4。
- 这个宏任务执行完毕,然后执行微任务队列:输出
3。
重点:await 产生的微任务次数
在 asy2 函数中:
await一个非Promise值(数字)会产生一个微任务(用于继续执行async函数后面的代码,即使后面没有代码,也需要解决async函数返回的Promise)。
在asy1函数中:await asy2():asy2返回一个Promise,这个Promise的解决会在asy2函数内部的await完成时(即上面产生的微任务执行时)解决。然后,asy1函数中await后面的代码(输出2)会被放入微任务队列。
因此,整个流程中微任务队列的变化:
- 同步代码执行完毕后,微任务队列有两个微任务:
- 第一个是
asy2函数中await产生的(标记为微任务A):用于解决asy2函数返回的Promise。 - 第二个是
asy3放入的(微任务B):输出6。
- 执行微任务A:这会解决
asy2函数返回的Promise,然后导致asy1函数中的await完成,从而将asy1函数中await后面的代码(输出2)作为新的微任务(微任务C)加入队列。 - 执行微任务B:输出6。
- 然后执行微任务C:输出2。
所以,asy2函数中的await确实产生了一个微任务(微任务A),并且这个微任务的执行又导致了一个新的微任务(微任务C)产生。
结论
asy2 函数中的 await 产生了微任务。整个代码中,微任务队列的执行顺序是:
- 微任务A(来自
asy2的await):解决asy2的Promise,触发asy1中的await完成,将输出2加入微任务队列(微任务C)。 - 微任务B(来自
asy3):输出6。 - 微任务C(来自
asy1的await完成):输出2。
然后执行宏任务(定时器回调),在定时器回调中又产生了一个微任务(输出3),最后执行。
1 | async function asy1(params) { |
分析如下:
- 声明asy1、asy2、asy3函数,不进入执行栈。
- 调用
asy1(),进入asy1函数:
- 输出
1 - 调用
asy2()
- 在
asy2中:
await (() => {console.log(3) })()即await console.log(3)最终会生成一个立即解决的Promise,即await Promise.resolve()记为P1,该Promise解决后,将console.log(4);(回调A)推入微任务队列。- 此时
await (async () => {...)()返回的Promise,记为P2,需要等待内部的console.log(4)执行后才完成,所以该Promise挂起,asy1()中暂停执行。
console.log(7)执行,输出7。- 调用
asy3(),将console.log(6)(回调B)加入微任务队列中,此时微任务队列:[回调A(console.log(4)),回调B(console.log(6))] - 执行微任务队列(阶段1):
- 回调A调用,输出4。P2完成,
await将后续代码(没有)推入微任务队列,所以会产生一个微任务解析asy2()的完成(微任务C)。 - 回调B调用,输出6。此时微任务队列:[微任务C(asy2的完成)]
- 执行微任务队列(阶段2):
- 回调C调用,
asy1()中await asy2()完成,将后续代码(console.log(2)回调D)推入微任务队列。 - 执行回调D,输出2。
- Title: 关于eventloop
- Author: cenweilings@163.com
- Created at : 2023-10-08 00:00:00
- Updated at : 2025-09-27 15:11:56
- Link: https://blog-git-main-cenweilings-projects.vercel.app/2023/10/08/关于eventloop/
- License: This work is licensed under CC BY-NC-SA 4.0.