核心概念:同步 vs 异步
- JavaScript 是一门单线程语言,这意味着它同一时间只能执行一个任务;但单线程会导致一个问题:如果遇到耗时操作 (比如网络请求、定时器、文件读取),程序会被阻塞,页面会卡死;
- 为了解决这个问题,JS 设计了同步和异步两种执行模式;
同步
-
定义:任务按顺序执行,前一个任务完成后,后一个任务才能开始;
-
特点:阻塞执行,代码自上而下逐行运行,每一步都要等上一步结束;
-
例子:
function synchronousExample() { console.log('开始同步操作'); // 模拟耗时操作 const start = Date.now(); while (Date.now() - start < 3000) { // 阻塞3秒 } console.log('同步操作完成'); return '结果'; } console.log('调用前'); const result = synchronousExample(); // 这里会阻塞3秒 console.log('调用后,结果:', result);
异步
-
定义:不阻塞主线程的任务,耗时操作会被 “挂起”,等结果返回后再执行回调;
-
特点:非阻塞执行,耗时任务不会卡住主线程,主线程继续执行同步任务;
-
常见异步场景:
- 定时器 (setTimeout、setInterval);
- 网络请求 (fetch、axios、XMLHttpRequest);
- 事件监听 (click、scroll);
- Promise、async/await;
- 文件操作 (Node.js 中);
-
例子:
function asynchronousExample() { console.log('开始异步操作'); // 使用回调 setTimeout(() => { console.log('异步操作完成'); }, 3000); console.log('函数立即返回'); return '立即返回值'; } console.log('调用前'); const result = asynchronousExample(); // 不会阻塞 console.log('调用后,立即结果:', result); // 输出顺序: // 调用前 // 开始异步操作 // 函数立即返回 // 调用后,立即结果: 立即返回值 // (3秒后)异步操作完成
事件循环(Event Loop):JS 异步的核心运行机制
-
先搞懂 JS 的执行环境结构:JS 运行时会划分不同的 “任务队列”,核心分为:
- 调用栈 (Call Stack):也叫执行栈,存放正在执行的同步任务,遵循 “先进后出” 原则;
- 任务队列 (Task Queue):也叫宏任务队列,存放待执行的异步宏任务 (比如定时器回调、事件回调、ui 渲染、script 脚本、requestAnimationFrame 等);
- 微任务队列 (Microtask Queue):优先级高于宏任务队列,存放微任务 (比如 Promise.then/catch/finally、async/await、queueMicrotask、process.nextTick 等);
-
事件循环的完整执行流程
- 执行调用栈中的同步任务,直到调用栈为空;
- 执行微任务队列中的所有微任务 (按顺序),直到微任务队列为空;
- 从宏任务队列中取出第一个宏任务,放入调用栈执行;
- 重复步骤 1-3,形成 “循环”;
案例分析
<body>
<script>
document.body.style.background = 'green';
console.log(1);
Promise.resolve().then(() => {
console.log(2);
document.body.style.background = 'red';
});
console.log(3);
// 1 3 2 red(只渲染红色)
</script>
</body>
<body>
<script>
document.body.style.background = 'green';
console.log(1);
setTimeout(() => {
console.log(2);
document.body.style.background = 'red';
}, 10);
console.log(3);
// 1 3 2 green => red(屏幕闪烁)
</script>
</body>
面试题
第 1 题
-
题目
let n = 0; // 设置定时器的操作是同步的,但是 1S 后做的事情是异步的 setTimeout(_ => { n += 10; console.log(n); }, 1000); n += 5; console.log(n); -
图解
第 2 题
-
题目
setTimeout(() => { console.log(1); }, 20); console.log(2); setTimeout(() => { console.log(3); }, 10); console.log(4); // 计时开始 console.time('AA'); for (let i = 0; i < 90000000; i++) { // do soming 280ms左右 } // 计时结束 console.timeEnd('AA'); console.log(5); setTimeout(() => { console.log(6); }, 8); console.log(7); setTimeout(() => { console.log(8); }, 15); console.log(9); -
图解
第 3 题
console.log(1);
setTimeout(_ => console.log(2), 50);
console.log(3);
setTimeout(_ => {
console.log(4);
// 遇到死循环,所有代码执行最后都是在主栈中执行,遇到死循环,主栈永远结束不了,后面啥都干不了
while (1 === 1) { }
}, 0);
console.log(5);
第 4 题
<body>
<button id="button">按钮</button>
<script>
button.addEventListener('click', () => {
console.log('listener1');
Promise.resolve().then(() => console.log('micro task1'))
})
button.addEventListener('click', () => {
console.log('listener2');
Promise.resolve().then(() => console.log('micro task2'))
})
button.click(); // 相当于函数执行 click1() click2(),此时并未将回调放到 宏任务队列中
// listener1
// listener2
// micro task1
// micro task2
</script>
</body>
<body>
<!-- 点击按钮 -->
<button id="button">按钮</button>
<script>
button.addEventListener('click', () => {
console.log('listener1');
Promise.resolve().then(() => console.log('micro task1'))
})
button.addEventListener('click', () => {
console.log('listener2');
Promise.resolve().then(() => console.log('micro task2'))
})
// 点击按钮执行事件,将两个事件回调都放到了宏任务队列中,每次拿出一个执行
// listener1
// micro task1
// listener2
// micro task2
</script>
</body>
第 3️⃣ 座大山:浏览器的渲染机制、重绘、回流
上一篇