注:本文是学习事件循环后的个人笔记,建议配合下列参考资料一起阅读。
资料来自
- MDN:事件循环 - JavaScript | MDN - MDN 文档
- Jake Archibald event loop演讲: Jake Archibald_ In The Loop
- B站up住董学长:面试还不会判断宏任务和微任务的输出顺序
- 和 Claude 的问答式互动的结果
为什么要有事件循环
Javascript是一个单线程的编程语言。
所谓单线程,指语言同一时间只能执行一个任务,即如果执行任务a花费了很长的时间,那么后面的任务b、c、别无它法,只能等待直到任务a执行完毕。
演示代码:点击后整个页面会暂停,因为while阻塞了线程,导致后面的任务无法执行。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>事件循环演示</title>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background: #111;
color: #eee;
font-family: monospace;
gap: 32px;
}
#track {
width: 500px;
height: 80px;
border: 1px solid #333;
border-radius: 8px;
position: relative;
}
#box {
width: 60px;
height: 60px;
background: #7c6aff;
border-radius: 8px;
position: absolute;
top: 10px;
left: 0;
}
button {
padding: 12px 28px;
background: #ff5f5f22;
border: 1px solid #ff5f5f66;
color: #ff5f5f;
border-radius: 8px;
font-family: monospace;
font-size: 14px;
cursor: pointer;
}
p {
color: #666;
font-size: 13px;
}
</style>
</head>
<body>
<p>box 由 requestAnimationFrame 驱动</p>
<div id="track">
<div id="box"></div>
</div>
<button onclick="blockThread()">点击阻塞主线程 while(true)</button>
<p id="status">主线程运行中</p>
<script>
const box = document.getElementById('box');
const status = document.getElementById('status');
let pos = 0;
let dir = 1;
function animate() {
pos += 2 * dir;
if (pos >= 440) dir = -1;
if (pos <= 0) dir = 1;
box.style.left = pos + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
function blockThread() {
status.textContent = '主线程已阻塞——页面冻结';
// 让当前任务先结束,浏览器渲染文字更新
// 再通过 setTimeout 把 while(true) 放进下一个任务
setTimeout(() => {
while (true) {}
}, 0);
}
</script>
</body>
</html>
为了让单线程也能像多线程一样并发多个执行任务,诞生了事件循环机制
事件循环的组成
组成部分
- 执行栈
- 堆
- 宏任务队列(消息队列)
- 微任务队列
执行栈
用于跟踪某个函数的执行,每次调用一个函数,就把它压进栈顶;函数执行完,就从栈顶弹出。
流程示意图
function sayHi() {
console.log("hello world");
}
function greeting() {
sayHi();
}
greeting();
初始状态:[]
执行 greeting() 时:[greeting()]
执行 sayHi() 时:[greeting(), sayHi()]
// 控制台输出 hello world
sayHi() 执行完毕,出栈:[greeting()]
greeting() 执行完毕,出栈:[]
堆
堆用于存放引用类型(对象、数组、函数等)的实际数据。栈里的变量只存一个指针,指向堆里的本体。 堆里的数据生命周期比栈长——只要还有引用指向它,GC 就不会清理它。闭包就是一个典型例子:外层函数的 Frame 出栈了,但被内层函数引用的变量依然活在堆里的闭包环境对象中。
流程示意图
function outer() {
const x = 10;
return function inner() {
console.log(x);
};
}
const fn = outer();
fn();
此时在堆中创建了(以下只是示意,实际上它们是堆里分开存储的独立对象(不同的内存地址上))
// 闭包环境对象
closureEnv: { x: 10 },
// inner 函数对象,持有指向闭包环境的引用
inner: function() { console.log(x) } // → 指向 closureEnv
宏任务队列(消息队列)
用于存放宏任务,只有在前一个宏任务衍生的所有微任务被执行完后才会执行
常见宏任务:
- 整个JS脚本
setTimeout/setInterval- 用户交互事件(点击、键盘输入等)
- 网络请求完成(
fetch、XMLHttpRequest) - 页面加载事件
微任务队列
用于存放微任务,只会在宏任务执行结束后才会执行,并且只会在执行完整个微任务队列(包括执行微任务过程中新产生的微任务)后才进入下一个宏任务。
常见微任务:
Promise.then/Promise.catch/Promise.finallyasync/await(await后面的代码)queueMicrotask
任务执行顺序
在前文中了解了各个组件的职责,接下来看看它们如何协作运转:
JavaScript 引擎会不断重复以下循环:
- 从宏任务队列中取出一个任务执行(初始时,整个脚本就是一个宏任务)。
- 执行过程中遇到函数调用,将其压入执行栈;遇到异步 API,将其回调放入对应的任务队列。
- 当前宏任务执行完毕后,立即依次清空微任务队列中所有微任务(包括执行期间新产生的微任务)。
- 必要时进行页面渲染(Style、Layout、Paint)。
- 回到第 1 步,继续取下一个宏任务。
伪代码示意
while (true) {
const macroTask = macroTaskQueue.dequeue();
execute(macroTask);
while (microTaskQueue.hasTask()) {
const microTask = microTaskQueue.dequeue();
execute(microTask);
}
if (shouldRender()) render();
}
练习题
题目
// 练习1:经典输出题,写出输出顺序
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:?
// 练习2:复杂版
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => console.log('promise inside timeout'));
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => console.log('timeout inside promise'), 0);
});
console.log('end');
// 输出:?
// 练习3:async/await 版本
async function foo() {
console.log('foo start');
await bar();
console.log('foo end');
}
async function bar() {
console.log('bar');
}
console.log('script start');
foo();
console.log('script end');
// 输出:?
答案
// 练习1
1
4
3
2
// 练习2
start
end
promise1
timeout1
promise inside timeout
timeout inside promise
// 练习3(await bar() 后的 console.log('foo end') 相当于微任务。)
script start
foo start
bar
script end
foo end