首先, 咱们得明白JavaScript是单线程的. 啥意思呢? 就是说JS在同一时间只能做一件事, 不能像Java那样开多个线程同时干活. 这听起来是不是很弱鸡? 别急, 设计成这样是有原因的.
想象一下, 如果JS是多线程的, 你在一个线程里修改DOM, 另一个线程里删除同一个DOM, 那浏览器该听谁的? 这不就乱套了吗? 为了避免这种混乱, JS选择了单线程.
但单线程也有个问题--如果遇到耗时操作(比如网络请求), 整个页面不就卡死了吗? 这时候事件循环机制就派上用场了! 它让JS虽然只有一个主线程, 但能够"看起来"同时处理多个任务.
二、事件循环机制
事件循环机制主要由三部分组成:
1. 调用栈(Call Stack): 这是JS执行代码的地方, 你可以把它想象成一个盘子, 后放进去的先拿出来(后进先出)
2. 任务队列(Task Queue): 等待执行的任务排成的队伍
3. 事件循环(Event Loop): 负责盯着调用栈和任务队列的"监工"
咱们来看个简单的例子:function multiply(a, b) {return a * b;}function square(n) {return multiply(n, n);}function printSquare(n) {const squared = square(n);console.log(squared);}printSquare(4);
执行过程是这样的:
1. printSquare(4)入栈
2. printSquare内部调用square(4),square(4)入栈
3. square内部调用multiply(4,4),multiply(4,4)入栈
4. multiply执行完返回16,出栈
5. square返回16,出栈
6. console.log(16)入栈,执行完出栈
7. printSquare执行完,出栈
整个过程就像叠盘子一样, 后进的先出.
那异步代码怎么执行呢? 看这个例子:console.log('Hi');setTimeout(function cb() {console.log('There');}, 5000);console.log('JS');
输出顺序是: 'Hi' -> 'JS' -> 'There'
执行过程:
1. console.log('Hi')入栈, 执行完出栈
2. 遇到setTimeout, 这是Web API提供的功能, JS把它交给浏览器处理, 同时注册回调函数cb
3. console.log('JS')入栈, 执行完出栈
4. 5秒后, 浏览器把cb放入任务队列
5. 事件循环发现调用栈空了, 就把cb从任务队列拿到调用栈执行
三、微任务与宏任务
这才是重点! 面试常考! JS的任务其实分为两种:
-
宏任务(Macrotask): 包括script整体代码、setTimeout、setInterval、I/O、UI渲染等.
-
微任务(Microtask): 包括Promise.then、MutationObserver、process.nextTick(Node环境).
它们执行顺序的区别很重要:
1. 执行一个宏任务(初始就是整个script)
2. 执行过程中遇到微任务,就加入微任务队列; 遇到宏任务,就加入宏任务队列
3. 当前宏任务执行完, 立即执行所有微任务(直到微任务队列清空)
4. 进行浏览器渲染(如果需要)
5. 开始下一个宏任务
来看个经典例子:
console.log('script start');setTimeout(function() {console.log('setTimeout');}, 0);Promise.resolve().then(function() {console.log('promise1');}).then(function() {console.log('promise2');});console.log('script end');
输出顺序是:
'script start' -> 'script end' -> 'promise1' -> 'promise2' -> 'setTimeout'
解释:
1. 首先执行整个script(这是一个宏任务)
2. 遇到console.log('script start')直接执行
3. 遇到setTimeout, 回调加入宏任务队列
4. 遇到Promise.then, 第一个then回调加入微任务队列
5. 执行console.log('script end')
6. 当前宏任务执行完毕, 开始执行微任务队列
7. 执行第一个then回调, 输出'promise1', 然后又遇到第二个then, 其回调加入微任务队列
8. 继续执行微任务队列,输出'promise2'
9. 微任务队列清空,开始下一个宏任务(setTimeout回调),输出'setTimeout'
四、浏览器与Node环境差异1) 浏览器中的事件循环浏览器的事件循环相对简单:
-
执行宏任务
-
执行所有微任务
-
可能需要渲染
-
重复
Node的事件循环分为多个阶段:
-
timers阶段: 执行setTimeout和setInterval回调
-
I/O callbacks阶段: 执行几乎所有的回调(除了close事件、timers和setImmediate)
-
idle, prepare阶段: 内部使用
-
poll阶段: 检索新的I/O事件,执行相关回调
-
check阶段: 执行setImmediate回调
-
close callbacks阶段: 执行关闭事件的回调(如socket.on('close'))
在Node中, process.nextTick的优先级高于微任务Promise.then.
来看个Node的例子:
setTimeout(() => console.log('timeout'), 0);setImmediate(() => console.log('immediate'));Promise.resolve().then(() => console.log('promise'));process.nextTick(() => console.log('nextTick'));
输出顺序可能是:
'nextTick' -> 'promise' -> 'timeout' -> 'immediate'
解释:
-
process.nextTick优先级最高 -
然后是微任务Promise.then
-
然后是timers阶段的setTimeout
-
最后是check阶段的setImmediate
![]() |
Austin Liu 刘恒辉
Project Manager and Software Designer E-Mail:lzhdim@163.com Blog:https://lzhdim.cnblogs.com 欢迎收藏和转载此博客中的博文,但是请注明出处,给笔者一个与大家交流的空间。谢谢大家。 |




浙公网安备 33010602011771号