事件循环event loop (通俗易懂版)
一 堆、栈、列
1.1 栈(Stack):像叠盘子,先进后出
想象你在餐厅收拾盘子,洗干净的盘子会一个一个往上叠。拿的时候呢?只能从最上面那个开始拿,不能从中间抽,也不能从底下掏。
- 特点:最后放进去的,最先被拿走(先进后出,LIFO)
- 例子:
- 函数调用栈:你调用
A(),A()里又调用B(),B()执行完才能回到A(),最后A()执行完才结束(像叠盘子一样,B压在A上面,先拿B) - 浏览器的前进后退其实也类似栈:打开新页面是 “压栈”,点后退是 “弹栈”
- 函数调用栈:你调用
1.2 队列(Queue):像排队买奶茶,先进先出
奶茶店排队,第一个来的人先点单,第二个排在后面,必须等前面的人都走完,后面的才能轮到。
- 特点:最先放进去的,最先被拿走(先进先出,FIFO)
- 例子:
- 事件队列:浏览器里的点击事件、定时器回调,会按触发顺序排队,JS 引擎一个一个处理(先触发的事件先执行)
- 消息队列:比如 React 的更新任务,会按优先级排队,依次执行
1.3 堆(Heap):像杂乱的储物间,按需查找
你有个储物间,东西随便堆,但每个东西上贴了标签(比如 “最贵的”“最大的”)。需要的时候不用按顺序翻,直接根据标签找(比如每次都先拿 “最贵的”)。
- 特点:没有固定顺序,但能快速找到 “最值”(最大或最小)
- 例子:
- 优先级队列:比如任务调度,高优先级的任务(如用户输入)要比低优先级的(如日志打印)先执行,这时候就用堆来管理
- 内存分配:JS 里的对象数据存在堆里,变量只是引用它(就像你记得储物间里某个东西的标签,直接去拿)
1.4 一句话总结区别
- 栈:叠盘子(后进先出)
- 队列:排队(先进先出)
- 堆:带标签的储物间(快速找最值)
二 事件循环(Event Loop)
2.1 先明确三个角色
- 你(JS 引擎):相当于后厨里唯一的厨师,一次只能做一件事(单线程)。
- 任务(Tasks):客人点的菜,分两种:
- 同步任务:简单的菜(比如拍个黄瓜),你能立刻做完。
- 异步任务:复杂的菜(比如炖鸡汤),你做不了,得交给别人(浏览器的其他线程,比如定时器线程、网络线程)。
- 任务队列:像取餐台,异步任务做好后会放在这里排队,等你有空了就来拿。
2.2 事件循环的过程:就像厨师的工作流程
-
先处理同步任务 你面前有一堆菜,先挑同步任务(简单的),按顺序一个接一个做,做完为止。
比如:执行
console.log('1')(同步),直接输出;遇到setTimeout(...)(异步),就交给 “定时器师傅” 处理,自己继续做下一个同步任务。 -
异步任务交给 “帮手” 像
setTimeout(等时间)、fetch(发请求)、click事件(用户点击)这些异步任务,你处理不了,就分给浏览器的其他 “师傅”(线程):- 定时器师傅负责计时,时间到了就把回调函数放到任务队列里。
- 网络师傅负责发请求,拿到数据后把回调放到任务队列里。
- 事件师傅负责监听点击,用户点了就把事件处理函数放到任务队列里。
-
同步任务做完了,就去 “取餐台” 找任务 当你把所有同步任务都做完(当前 “执行栈” 空了),就会去看任务队列:
- 队列里有任务的话,就按顺序(先进先出)拿一个过来执行。
- 执行完这个异步任务的回调后,再检查队列里还有没有任务,有就再拿一个,循环往复。
2.3 例子:为什么 setTimeout 不准时?
console.log('1'); // 同步任务,先执行
setTimeout(() => {
console.log('2'); // 异步任务,交给定时器师傅
}, 0);
console.log('3'); // 同步任务,继续执行
- 你先做同步任务:输出
1→ 输出3(此时同步任务做完)。 - 再去任务队列看,发现定时器师傅放了
console.log('2'),就执行它,输出2。 - 哪怕
setTimeout写0毫秒,也得等同步任务做完才会执行,所以它的 “准时” 是相对于队列顺序,不是绝对时间。
2.4 一句话总结
事件循环就是 “先干完手头的活(同步任务),再去队列里按顺序拿新活(异步任务)” 的循环过程。因为 JS 是单线程(一个厨师),靠这个机制才能同时 “处理”(其实是排队处理)各种异步操作,比如定时器、网络请求、用户事件。
三 微任务和宏任务
还是用 “餐厅后厨” 的场景接着讲,这次加入 “VIP 客户” 的设定
3.1 先明确:微任务和宏任务都是异步任务(都要进队列等 JS 引擎处理),但优先级不同。
- 宏任务(Macro Task):普通客人点的菜,优先级低。
- 微任务(Micro Task):VIP 客人点的菜,优先级高。
3.2 具体场景
假设你(JS 引擎)刚做完所有同步任务(手头的活干完了),现在要处理任务队列里的异步任务。这时候规则变了:
-
先清微任务队列(VIP 优先) 任务队列分两个:一个是 “微任务队列”(VIP 专属),一个是 “宏任务队列”(普通客人)。
你会先把微任务队列里的所有任务按顺序全做完(不管有多少个),一个都不能剩。
-
再做一个宏任务 微任务全清完后,才去宏任务队列里拿一个任务来做(注意:只拿一个)。
-
然后再回到微任务队列 这个宏任务做完后,不管宏任务队列里还有没有其他任务,都要先回头检查微任务队列:如果有,还是先把所有微任务全清完,再去拿下一个宏任务。
循环往复……
3.3 哪些是微任务?哪些是宏任务?
微任务(VIP):
- Promise 的
then/catch/finally(比如new Promise(...).then(...)里的回调) async/await(本质是 Promise 的语法糖,await后面的代码都是微任务)queueMicrotask()(直接创建一个微任务)
宏任务(普通客人):
setTimeout、setInterval(定时器)- DOM事件(比如 click、scroll 的回调)
fetch等网络请求的回调script标签整体(整个 JS 文件的执行,算是第一个宏任务)
3.4 例子:看执行顺序
// 同步任务:先执行
console.log('1');
// 宏任务:扔进宏任务队列
setTimeout(() => {
console.log('2');
}, 0);
// 微任务:扔进微任务队列
Promise.resolve().then(() => {
console.log('3');
});
// 同步任务:继续执行
console.log('4');
执行步骤:
- 先做同步任务:输出
1→ 输出4(同步任务完)。 - 检查微任务队列:有一个
console.log('3'),执行 → 输出3(微任务全清)。 - 去宏任务队列拿一个任务:
console.log('2'),执行 → 输出2。
最终结果:1 → 4 → 3 → 2(微任务比宏任务先执行)。
3.5 一句话总结
- 微任务是 “VIP”,宏任务是 “普通客人”。
- 事件循环的规则:做完同步任务 → 清完所有微任务 → 做一个宏任务 → 再清完所有微任务 → 做下一个宏任务……
这就是为什么Promise.then总是比setTimeout先执行 —— 因为微任务优先级更高,必须 “插队” 先做完~