JavaScript 的事件循环机制
JavaScript 的事件循环(Event Loop)是其实现异步编程的核心机制,它使得 JavaScript 虽然是单线程语言,却能高效处理异步操作。
基本概念
- 单线程特性:JavaScript 是单线程的,同一时间只能执行一个任务
- 非阻塞 I/O:通过事件循环实现异步操作,避免阻塞主线程
- 并发模型:基于"事件循环"的并发模型处理多个任务
事件循环的组成部分
1. 调用栈(Call Stack)
- 后进先出(LIFO)的数据结构
- 存储函数的调用信息(执行上下文)
- 当函数执行时会被推入栈,执行完毕则弹出
2. 任务队列(Task Queue)
- 先进先出(FIFO)的数据结构
- 存储待处理的异步任务回调
- 分为宏任务队列和微任务队列
3. 事件循环进程
- 不断检查调用栈是否为空
- 当调用栈为空时,从任务队列中取出任务执行
任务类型
宏任务(Macro Tasks)
- script 整体代码
- setTimeout/setInterval
- I/O 操作
- UI 渲染
- setImmediate(Node.js)
微任务(Micro Tasks)
- Promise.then/catch/finally
- MutationObserver(浏览器)
- process.nextTick(Node.js,优先级最高)
事件循环的执行顺序
-
执行全局同步代码(宏任务)
-
执行过程中:
- 同步代码直接执行
- 遇到宏任务回调,放入宏任务队列
- 遇到微任务回调,放入微任务队列
-
当前宏任务执行完毕
-
检查微任务队列并执行所有微任务
-
如有必要进行 UI 渲染
-
从宏任务队列取出下一个宏任务执行
-
循环往复
示例代码分析
javascript
console.log('1. 开始');
setTimeout(() => {
console.log('4. setTimeout 回调');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise 回调');
});
console.log('2. 结束');
输出顺序:
text
1. 开始
2. 结束
3. Promise 回调
4. setTimeout 回调
Node.js 与浏览器的事件循环差异
-
Node.js:
- 有多个阶段(timers、pending callbacks、idle/prepare、poll、check、close callbacks)
- process.nextTick 优先级高于微任务
-
浏览器:
- 结构相对简单
- 微任务在每个宏任务之后执行
JavaScript 调用栈(Call Stack)详解
调用栈是 JavaScript 执行机制中最基础、最重要的组成部分之一,它直接决定了代码的执行顺序和函数调用的处理方式。
什么是调用栈?
调用栈是一种后进先出(LIFO) 的数据结构,用于记录函数调用关系和管理执行上下文。它跟踪程序当前执行的位置,当函数被调用时会被推入栈顶,执行完毕后从栈顶弹出。
调用栈的工作原理
- 函数调用时:将函数及其执行上下文推入栈顶
- 函数执行时:使用栈顶的执行上下文
- 函数返回时:将该函数从栈顶弹出
- 栈空时:程序执行结束
调用栈的特点
-
单线程:JavaScript 只有一个调用栈,同一时间只能执行一个任务
-
同步执行:栈中的函数会一直执行到完成,不会被其他函数打断
-
有限容量:调用栈有最大深度限制(栈溢出保护)
- 现代浏览器通常限制在 10,000-50,000 层左右
- 递归过深会导致 "Maximum call stack size exceeded" 错误
调用栈示例分析
javascript
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const squared = square(n);
console.log(squared);
}
printSquare(4);
执行过程解析:
-
printSquare(4)
被调用 → 推入栈- 调用栈:[printSquare]
-
printSquare
调用square(4)
→square
推入栈- 调用栈:[printSquare, square]
-
square
调用multiply(4, 4)
→multiply
推入栈- 调用栈:[printSquare, square, multiply]
-
multiply
执行完成,返回 16 → 弹出栈- 调用栈:[printSquare, square]
-
square
返回结果 → 弹出栈- 调用栈:[printSquare]
-
console.log(16)
被调用 → 推入栈- 调用栈:[printSquare, console.log]
-
console.log
执行完成 → 弹出栈- 调用栈:[printSquare]
-
printSquare
执行完成 → 弹出栈- 调用栈:[]
调用栈与异步代码
对于异步操作,调用栈的处理方式特殊:
javascript
console.log('开始');
setTimeout(() => {
console.log('回调');
}, 0);
console.log('结束');
执行过程:
- 同步代码按顺序执行并推入/弹出调用栈
- setTimeout 的回调函数会被放入任务队列
- 只有当调用栈为空时,事件循环才会将回调函数推入调用栈执行
调用栈的调试技巧
-
使用开发者工具:
- Chrome DevTools 的 Sources 面板可以查看当前调用栈
- 断点调试时,Call Stack 面板显示完整的调用链
-
错误堆栈跟踪:
- 当错误发生时,错误信息会显示完整的调用栈
- 有助于定位问题源头
-
console.trace() :
- 在代码中插入
console.trace()
可以打印当前调用栈
- 在代码中插入
调用栈的常见问题
-
栈溢出(Stack Overflow) :
- 通常由无限递归引起
javascriptfunction infiniteLoop() { infiniteLoop(); // 无限递归 } infiniteLoop();
-
阻塞主线程:
- 长时间运行的同步代码会阻塞调用栈
- 导致页面无响应
-
内存泄漏:
- 不当的闭包使用可能导致执行上下文无法从栈中释放
最佳实践
- 避免过深的递归调用,考虑使用迭代替代
- 将计算密集型任务拆分为小块,使用 setTimeout/setInterval 分步执行
- 合理使用异步编程避免阻塞调用栈
- 注意闭包的使用,避免意外保持对执行上下文的引用