目录
第八章 事件循环与执行机制
本章目标
- 深入理解JavaScript的单线程模型
- 掌握调用栈(Call Stack)的工作原理
- 理解任务队列(Task Queue)和微任务队列(Microtask Queue)
- 掌握事件循环(Event Loop)的执行机制
- 学会分析异步代码的执行顺序
8.1 JavaScript的单线程模型
8.1.1 为什么是单线程
JavaScript是一门单线程编程语言,这意味着它在同一时间只能执行一个任务。这种设计有其历史原因和实际考量:
历史原因:
- JavaScript最初被设计用于浏览器中的表单验证和简单的DOM操作
- 如果多线程同时操作DOM,会导致复杂的同步问题
实际优势:
- 代码逻辑更简单,无需处理线程安全问题
- 避免了死锁、竞态条件等多线程常见问题
- 开发者可以更专注于业务逻辑
// 单线程示例 console.log('A'); console.log('B'); console.log('C'); // 输出必然是 A → B → C,顺序确定
8.1.2 异步的必要性
尽管JavaScript是单线程的,但它需要处理各种I/O操作(网络请求、文件读写、定时器等)。如果这些操作是同步的,会阻塞主线程,导致页面卡顿。
// 如果这是同步的,会阻塞3秒钟 const result = syncNetworkRequest(); // 阻塞3秒 console.log('请求完成'); // 在这3秒内,页面无法响应用户操作 // 异步方式不会阻塞 asyncNetworkRequest(function(result) { console.log('请求完成'); }); console.log('继续执行其他代码');
8.2 调用栈(Call Stack)
8.2.1 什么是调用栈
调用栈是一种数据结构,用于记录程序执行的位置。当一个函数被调用时,它会压入栈顶;当函数执行完毕后,它会从栈顶弹出。
function first() { console.log('进入first函数'); second(); console.log('离开first函数'); } function second() { console.log('进入second函数'); third(); console.log('离开second函数'); } function third() { console.log('进入third函数'); console.log('离开third函数'); } first(); // 调用栈变化: // 1. first() 入栈 // 2. second() 入栈 // 3. third() 入栈 // 4. third() 出栈 // 5. second() 出栈 // 6. first() 出栈
8.2.2 栈溢出(Stack Overflow)
如果调用栈中的函数调用层数过多(通常是无限递归),会导致栈溢出错误。
// 错误的递归示例 function infiniteRecursion() { infiniteRecursion(); } // infiniteRecursion(); // RangeError: Maximum call stack size exceeded // 正确的递归应该有终止条件 function factorial(n) { if (n <= 1) return 1; return n * factorial(n - 1); } console.log(factorial(5)); // 120
8.3 任务队列(Task Queue)
8.3.1 宏任务(Macrotask)
宏任务是JavaScript中异步操作的一种类型,主要包括:
- setTimeout / setInterval
- setImmediate(Node.js环境)
- I/O操作
- UI渲染
- MessageChannel
console.log('1'); setTimeout(function() { console.log('2'); }, 0); console.log('3'); // 输出顺序:1 → 3 → 2 // 即使setTimeout的延迟为0,回调函数也是异步执行的
8.3.2 微任务(Microtask)
微任务是另一种异步操作类型,优先级高于宏任务,主要包括:
- Promise.then() / catch() / finally()
- MutationObserver
- queueMicrotask()
- process.nextTick(Node.js环境)
console.log('1'); setTimeout(function() { console.log('2(宏任务)'); }, 0); Promise.resolve().then(function() { console.log('3(微任务)'); }); console.log('4'); // 输出顺序:1 → 4 → 3(微任务) → 2(宏任务)
8.4 事件循环(Event Loop)
8.4.1 事件循环的工作流程
事件循环是JavaScript异步执行的核心机制,其工作流程如下:
1. 执行同步代码,清空调用栈 2. 执行所有微任务队列中的任务 3. 如果有必要,进行UI渲染 4. 从宏任务队列中取出一个任务执行 5. 重复步骤2-4
console.log('同步代码1'); setTimeout(function() { console.log('setTimeout 1'); Promise.resolve().then(function() { console.log('Promise 2'); }); }, 0); Promise.resolve().then(function() { console.log('Promise 1'); setTimeout(function() { console.log('setTimeout 2'); }, 0); }); console.log('同步代码2'); // 输出顺序分析: // 同步代码1 // 同步代码2 // Promise 1(微任务) // setTimeout 1(宏任务) // Promise 2(微任务,在setTimeout回调中产生) // setTimeout 2(宏任务)
8.4.2 事件循环的可视化
我们可以将事件循环想象成一个无限循环:
while (true) {
// 1. 执行同步代码
executeSyncCode();
// 2. 清空微任务队列
while (microtaskQueue.length > 0) {
executeMicrotask();
}
// 3. 渲染UI(如果需要)
renderUI();
// 4. 执行一个宏任务
if (macrotaskQueue.length > 0) {
executeMacrotask();
}
}
8.5 异步代码执行顺序分析
8.5.1 基础案例分析
案例1:基本执行顺序
console.log('A'); setTimeout(function() { console.log('B'); }, 0); Promise.resolve().then(function() { console.log('C'); }); console.log('D'); // 答案:A → D → C → B // 解释: // 1. 同步代码:A、D // 2. 微任务:C // 3. 宏任务:B
案例2:嵌套异步
console.log('1'); setTimeout(function() { console.log('2'); Promise.resolve().then(function() { console.log('3'); }); }, 0); Promise.resolve().then(function() { console.log('4'); setTimeout(function() { console.log('5'); }, 0); }); console.log('6'); // 答案:1 → 6 → 4 → 2 → 3 → 5
案例3:复杂的异步组合
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end'); // 答案: // script start // async1 start // async2 // promise1 // script end // async1 end // promise2 // setTimeout
8.5.2 async/await的执行分析
async/await是Promise的语法糖,理解其执行机制对掌握事件循环很重要。
async function test() { console.log('A'); await Promise.resolve(); console.log('B'); } console.log('C'); test(); console.log('D'); // 输出:C → A → D → B // await 之前的代码是同步执行的 // await 之后的代码相当于放入了微任务队列
8.6 实际应用场景
8.6.1 保证DOM更新后执行代码
// 错误的方式 function updateUI() { element.style.width = '100px'; console.log(element.offsetWidth); // 可能获取到旧值 } // 正确的方式:使用requestAnimationFrame或setTimeout function updateUI() { element.style.width = '100px'; requestAnimationFrame(function() { console.log(element.offsetWidth); // 获取到新值 }); }
8.6.2 批量处理与性能优化
// 将大量计算分批处理,避免阻塞主线程 function processLargeArray(array, chunkSize) { let index = 0; function processChunk() { const chunk = array.slice(index, index + chunkSize); // 处理当前批次 chunk.forEach(function(item) { // 处理item }); index += chunkSize; if (index < array.length) { // 使用setTimeout让出主线程 setTimeout(processChunk, 0); } } processChunk(); } // 使用 const largeArray = new Array(100000).fill(0); processLargeArray(largeArray, 1000);
8.6.3 实现nextTick
// 简单的nextTick实现 function nextTick(callback) { if (typeof Promise !== 'undefined') { Promise.resolve().then(callback); } else if (typeof MutationObserver !== 'undefined') { const observer = new MutationObserver(callback); const textNode = document.createTextNode('1'); observer.observe(textNode, { characterData: true }); textNode.data = '2'; } else { setTimeout(callback, 0); } } // 使用 console.log('1'); nextTick(function() { console.log('2'); }); console.log('3'); // 输出:1 → 3 → 2
8.7 浏览器与Node.js的差异
8.7.1 浏览器中的事件循环
浏览器中的事件循环主要处理:
- 用户交互事件
- 定时器
- 网络请求回调
- Promise回调
8.7.2 Node.js中的事件循环
Node.js的事件循环分为6个阶段:
1. **timers**:执行setTimeout和setInterval的回调 2. **I/O callbacks**:执行延迟到下一个循环迭代的I/O回调 3. **idle, prepare**:内部使用 4. **poll**:检索新的I/O事件 5. **check**:执行setImmediate回调 6. **close callbacks**:执行close事件的回调
// Node.js特有示例 setTimeout(function() { console.log('timeout'); }, 0); setImmediate(function() { console.log('immediate'); }); // 输出顺序不固定,取决于当前事件循环的状态
本章习题
基础练习
练习1:执行顺序预测 预测以下代码的输出顺序:
console.log('1'); setTimeout(function() { console.log('2'); }, 0); Promise.resolve().then(function() { console.log('3'); }); console.log('4');
练习2:复杂执行顺序 预测以下代码的输出:
setTimeout(function() { console.log('A'); }, 0); Promise.resolve().then(function() { console.log('B'); setTimeout(function() { console.log('C'); }, 0); }); Promise.resolve().then(function() { console.log('D'); }); console.log('E');
练习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');
进阶练习
练习4:实现sleep函数 使用Promise实现一个sleep函数,延迟指定时间后执行后续代码。
练习5:任务调度器 实现一个任务调度器,支持添加任务、设置优先级、控制并发数。
class TaskScheduler { constructor(concurrency) { // 实现 } add(task, priority) { // 实现 } }
练习6:微任务与宏任务的转换 解释以下代码的输出,并说明为什么:
Promise.resolve().then(function() { console.log('Promise 1'); setTimeout(function() { console.log('setTimeout 1'); }, 0); }); setTimeout(function() { console.log('setTimeout 2'); Promise.resolve().then(function() { console.log('Promise 2'); }); }, 0);
思考题
1. 为什么微任务的优先级高于宏任务?这种设计有什么好处? 2. async/await中的await关键字做了什么?它与Promise有什么关系? 3. 在浏览器中,如果微任务队列无限增长会发生什么?如何避免? 4. 解释requestAnimationFrame在事件循环中的位置。
参考答案
练习1答案: 输出顺序:1 → 4 → 3 → 2
练习2答案: 输出顺序:E → B → D → A → C
练习3答案: 输出顺序:script start → foo start → bar → script end → foo end
练习4答案:
function sleep(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } // 使用 async function example() { console.log('开始'); await sleep(1000); console.log('1秒后'); }
练习6答案: 输出顺序:Promise 1 → setTimeout 2 → Promise 2 → setTimeout 1
小结
本章我们深入学习了JavaScript的事件循环机制:
- 单线程模型:JavaScript是单线程语言,通过异步机制避免阻塞
- 调用栈:用于跟踪函数调用,后进先出(LIFO)
- 任务队列:分为宏任务队列和微任务队列
- 事件循环:不断从队列中取出任务执行的机制
- 执行优先级:同步代码 > 微任务 > 宏任务
理解事件循环对于编写高效、正确的异步代码至关重要。在下一章中,我们将学习JavaScript的面向对象编程。
