你是否曾在面试中被问及JavaScript的事件循环机制?是否曾对setTimeout的执行时机感到困惑?或者对Promise和setTimeout的优先级一头雾水?别担心,今天我们就来彻底解决这些问题!
为什么JS是单线程?
想象一下这样的场景:如果JavaScript是多线程的,一个线程要在某个DOM节点上添加内容,另一个线程却要删除这个节点,浏览器该听谁的呢?这就好比两个人同时试图驾驶一辆车,一个想左转一个想右转,结果可想而知。
为了避免这种混乱,JavaScript从诞生起就是单线程的。这意味着它一次只能做一件事,所有任务都需要排队执行。这种设计简化了编程难度,但同时也带来了一个问题:如果某个任务特别耗时,后续所有任务都会被阻塞,导致页面"卡死"。
执行栈与任务队列
执行栈
执行栈就像是一个装盘子的容器------最后放进去的盘子会被最先拿出来(后进先出)。当JavaScript开始执行代码时,它会创建一个全局执行上下文,并将其压入执行栈中。每当调用一个函数,就会创建一个新的函数执行上下文并压入栈顶。函数执行完毕后,它的执行上下文就会从栈中弹出。
javascript
function a() {
console.log('a');
b();
}
function b() {
console.log('b');
}
a();
// 执行顺序:a -> b
主线程
主线程是JavaScript执行同步代码的地方。所有同步任务都在这里按顺序执行,形成一个执行栈。当遇到异步操作时,JavaScript不会等待它完成,而是继续执行后面的代码。
JS异步执行的运行机制
既然JavaScript是单线程的,那它如何处理Ajax请求、定时器这类异步操作呢?答案就是:借助浏览器的多线程能力。
当JavaScript遇到异步操作时,会将它交给浏览器的其他线程(如定时器线程、HTTP请求线程等)处理,然后继续执行后面的同步代码。当异步操作完成后,相应的回调函数会被放入任务队列中等待执行。
这就好比在咖啡店点单:你点了一杯咖啡(发起异步请求),然后去找座位(继续执行同步代码),当咖啡做好后(异步操作完成),服务员会叫你的名字(回调函数进入任务队列),但你只有在空闲时(执行栈为空)才会去取咖啡(执行回调函数)。
宏任务与微任务
任务队列并非只有一个,而是分为两种类型:
宏任务(MacroTask) :包括setTimeout、setInterval、setImmediate(Node.js)、I/O操作、UI渲染等
微任务(MicroTask) :包括Promise.then/catch/finally、process.nextTick(Node.js)、MutationObserver等
它们的执行顺序很重要:当执行栈为空时,会先检查并清空所有微任务,然后才取出一个宏任务执行,然后再次检查微任务。
Event Loop(事件循环)
事件循环是JavaScript实现异步的核心机制,它的工作流程可以用以下代码简单表示:
scss
while (true) {
// 执行栈为空时开始循环
if (执行栈为空) {
// 1. 检查微任务队列,执行所有微任务
while (微任务队列中有任务) {
执行微任务();
}
// 2. 取出一个宏任务执行
if (宏任务队列中有任务) {
执行宏任务();
}
}
}
这个过程就像是一个永不停歇的循环,不断检查执行栈和任务队列,确保任务有序执行。
面试题实践
现在让我们通过一些面试题来巩固理解:
题目1:基本执行顺序
javascript
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// 输出顺序:1 -> 4 -> 3 -> 2
解析:
- 同步代码:先输出1和4
- 微任务:Promise.then是微任务,输出3
- 宏任务:setTimeout是宏任务,最后输出2
题目2:嵌套异步操作
javascript
console.log('start');
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('end');
// 输出顺序:start -> end -> promise1 -> promise2 -> timeout
题目3:综合挑战
javascript
console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('3');
resolve();
}).then(function() {
console.log('4');
});
}, 0);
new Promise(function(resolve) {
console.log('5');
resolve();
}).then(function() {
console.log('6');
});
setTimeout(function() {
console.log('7');
}, 0);
console.log('8');
// 输出顺序:1 -> 5 -> 8 -> 6 -> 2 -> 3 -> 4 -> 7
解析:
- 同步代码:输出1、5、8(Promise构造函数是同步执行的)
- 微任务:输出6
- 第一个setTimeout(宏任务):输出2、3(Promise构造函数同步执行)
- 第一个setTimeout中的then(微任务):输出4
- 第二个setTimeout(宏任务):输出7
总结
JavaScript的事件循环机制是前端开发中的核心概念,理解它对于编写高效、无阻塞的代码至关重要。记住几个关键点:
- JavaScript是单线程的,但借助浏览器的多线程能力实现异步
- 任务分为同步任务和异步任务,异步任务又分为宏任务和微任务
- 事件循环的顺序:同步任务 > 微任务 > 宏任务
- 微任务优先级高于宏任务,且会在当前宏任务结束后立即执行
希望这篇文章能帮助你彻底理解JavaScript的事件循环机制,下次面试时再遇到相关问题,就能从容应对了!