====== 第八章 事件循环与执行机制 ====== ===== 本章目标 ===== * 深入理解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的面向对象编程。