你是否曾好奇,为什么setTimeout
有时候"不准时"?为什么Promise.then
总能插队成功?今天,我们就来揭秘JavaScript中最核心的异步机制------事件循环(Event Loop),看看浏览器里的线程们是如何"约会"的。
一、进程与线程:浏览器里的"打工人"和"部门"
- 进程:相当于一个完整的"公司",比如打开一个Chrome标签页就是创建了一个新公司。它包含了所有资源和员工(线程),从成立到倒闭(关闭标签)的整个生命周期。
- 线程 :进程里的"打工人",比如负责和服务器聊天的
HTTP线程
、负责执行JS代码的引擎线程
、负责画画的渲染线程
。
有意思的是,JS引擎线程
和渲染线程
是对"冤家"------它们不能同时工作!就像公司里的两个部门抢会议室,一个用着另一个就得等着。这就是为什么JS是"单线程"的,同一时间只能干一件事。
二、异步:单线程的"时间管理大师"
JS既然是单线程,那遇到耗时任务(比如请求数据)怎么办?总不能干等着吧?于是它学会了"时间管理":
- 先把同步代码全部执行完(这是主线任务)
- 遇到异步代码,就把它暂时放到"任务队列"里排队
- 等主线任务干完了,再去"任务队列"里取异步代码执行
这就像你在公司上班:先处理完手头的紧急工作(同步代码),再去看邮件(异步任务)。
三、Event Loop:异步任务的"优先级排序"
异步任务也分"高低贵贱",Event Loop就是负责给它们排优先级的"HR":
1. 微任务:办公室里的"关系户"
微任务是最优先处理的,相当于公司里的"关系户",包括:
Promise.then
process.nextTick
(Node.js)MutationObserver
这些任务会在同步代码执行完后立即插队执行,而且会一直执行到队列为空。
2. 宏任务:老老实实排队的"普通员工"
宏任务就得老老实实排队,包括:
setTimeout
/setInterval
AJAX请求
I/O操作
UI渲染
3. 执行顺序:就像吃火锅
Event Loop的执行顺序可以类比吃火锅:
- 先吃主食(同步代码)
- 再吃小料(微任务)
- 然后涮肉(渲染页面)
- 最后煮面条(执行宏任务,开启下一轮循环)
四、代码实战:Event Loop的"狼人杀"
来看个经典例子,猜猜输出顺序:
javascript
console.log('script start');
async function async1() {
await async2()
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
async1()
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve) => {
console.log('promise');
resolve()
}).then(() => {
console.log('then1');
}).then(() => {
console.log('then2');
});
console.log('script end');
正确答案:
arduino
script start
promise
script end
async2 end
async1 end
then1
then2
setTimeout
是不是很神奇?这里的关键是:await
会先执行右边的代码,然后把后续代码扔入微任务队列。
五、await:异步世界的"插队小能手"
async/await
是Promise的语法糖,但它有个特殊能力:
- 会把后续代码挤入微任务队列
- 浏览器会"提前"执行
await
后面的代码(相当于同步)
就像你在排队买奶茶,突然有个人拿着await
的VIP卡,先点单(执行右边代码),然后去旁边等着(后续代码入微任务),等前面的人都买完了,他再回来取奶茶。
总结:Event Loop的"潜规则"
- 同步代码先执行(宏任务的一部分)
- 微任务队列清空后才会执行宏任务
- 每个宏任务执行完后,会检查微任务队列
await
后面的代码是微任务
理解了Event Loop,你就能解释为什么有些代码的执行顺序总是超出预期,也能写出更高效的异步代码。下次面试遇到这类问题,记得用"公司部门"