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