您现在的位置是:网站首页 > 单线程与事件循环文章详情
单线程与事件循环
陈川
【
JavaScript
】
2769人已围观
6364字
单线程的本质
JavaScript 是单线程语言,这意味着它一次只能执行一个任务。这种设计源于其最初作为浏览器脚本语言的定位,需要与 DOM 操作紧密配合。单线程避免了多线程环境下的竞态条件和锁机制等复杂问题,但也带来了阻塞的风险。
console.log('Start');
for(let i=0; i<1000000000; i++) {} // 长时间循环
console.log('End');
// 在这段代码中,'End'会等待循环完成后才输出
事件循环机制
为了处理异步操作而不阻塞主线程,JavaScript 引入了事件循环模型。这个模型由调用栈、任务队列和微任务队列组成:
- 调用栈:存储同步任务的执行上下文
- 任务队列(宏任务队列):存放 setTimeout、setInterval、I/O 等异步回调
- 微任务队列:存放 Promise.then、MutationObserver 等微任务
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
console.log('Script end');
/*
输出顺序:
Script start
Script end
Promise 1
Promise 2
setTimeout
*/
执行顺序详解
事件循环的具体工作流程可以分为以下几个阶段:
- 执行全局同步代码(属于宏任务)
- 检查微任务队列并执行所有微任务
- 渲染页面(如果需要)
- 从宏任务队列取出一个任务执行
- 重复上述过程
setTimeout(() => console.log('timeout1'), 0);
setTimeout(() => {
console.log('timeout2');
Promise.resolve().then(() => console.log('promise inside timeout'));
}, 0);
Promise.resolve().then(() => console.log('promise1'));
Promise.resolve().then(() => console.log('promise2'));
/*
输出顺序:
promise1
promise2
timeout1
timeout2
promise inside timeout
*/
浏览器与Node.js的差异
虽然都采用事件循环,但浏览器和Node.js的实现存在差异:
浏览器环境:
- 微任务在渲染前执行
- 每个宏任务后都会检查微任务队列
Node.js环境:
- 分为多个阶段(timers、pending callbacks等)
- 微任务在阶段切换时执行
- process.nextTick优先级高于Promise
// Node.js中的示例
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
/*
典型输出顺序:
nextTick
promise
timeout
immediate
*/
常见的异步模式
理解事件循环有助于编写高效的异步代码:
- Promise链式调用
function fetchData() {
return new Promise(resolve => {
setTimeout(() => resolve('data'), 1000);
});
}
fetchData()
.then(data => {
console.log(data);
return data.toUpperCase();
})
.then(modified => {
console.log(modified);
});
- async/await语法糖
async function process() {
try {
const data = await fetchData();
const result = await processData(data);
console.log(result);
} catch (error) {
console.error(error);
}
}
- 事件发射器模式
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('data', (data) => {
console.log('Received:', data);
});
setTimeout(() => {
emitter.emit('data', 'sample data');
}, 500);
性能优化实践
合理利用事件循环特性可以提升应用性能:
- 长任务拆分
// 不佳的做法
function processLargeArray(array) {
for(let i=0; i<array.length; i++) {
// 耗时处理
}
}
// 改进方案
async function processLargeArray(array) {
const CHUNK_SIZE = 100;
for(let i=0; i<array.length; i+=CHUNK_SIZE) {
const chunk = array.slice(i, i+CHUNK_SIZE);
await new Promise(resolve =>
requestAnimationFrame(() => {
processChunk(chunk);
resolve();
})
);
}
}
- 优先使用微任务
// 需要立即执行但不阻塞渲染的操作
function saveState() {
Promise.resolve().then(() => {
localStorage.setItem('appState', JSON.stringify(state));
});
}
- 合理使用Web Workers
// 主线程
const worker = new Worker('worker.js');
worker.postMessage(largeData);
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
// worker.js
self.onmessage = (e) => {
const result = process(e.data);
self.postMessage(result);
};
常见的陷阱与误区
- 误认为setTimeout(fn,0)会立即执行
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
// timeout不会最先执行
- 忽略微任务的递归风险
function recursivePromise() {
Promise.resolve().then(() => {
console.log('微任务执行');
recursivePromise(); // 会导致无限微任务循环
});
}
- 混淆宏任务与微任务的执行时机
setTimeout(() => console.log('timeout1'), 0);
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => console.log('timeout2'), 0);
});
/*
promise1
timeout1
timeout2
*/
实际应用场景
- 用户输入防抖
let timeoutId;
input.addEventListener('input', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
// 实际处理逻辑
}, 300);
});
- 动画队列管理
function animate(element) {
return new Promise(resolve => {
element.classList.add('fade-in');
element.addEventListener('animationend', resolve, {once: true});
});
}
async function runAnimations() {
await animate(document.querySelector('.box1'));
await animate(document.querySelector('.box2'));
}
- 数据批量处理
const batchSize = 100;
let currentIndex = 0;
function processBatch() {
const end = Math.min(currentIndex + batchSize, data.length);
while(currentIndex < end) {
processItem(data[currentIndex++]);
}
if(currentIndex < data.length) {
setTimeout(processBatch, 0);
}
}
深入理解任务优先级
浏览器环境中不同任务的优先级:
- 用户交互事件(点击、滚动等)具有最高优先级
- 渲染相关任务(requestAnimationFrame)
- 微任务(Promise回调)
- 宏任务(setTimeout、setInterval)
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask from click'));
setTimeout(() => console.log('Timeout from click'), 0);
});
setTimeout(() => {
console.log('Regular timeout');
}, 0);
// 点击按钮后的输出顺序可能:
// Microtask from click
// Regular timeout
// Timeout from click
Node.js中的特殊案例
Node.js的事件循环包含更多阶段:
- timers:执行setTimeout和setInterval回调
- pending callbacks:执行系统操作回调
- idle/prepare:内部使用
- poll:检索新的I/O事件
- check:执行setImmediate回调
- close callbacks:执行关闭事件回调
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
// 输出顺序可能不同,取决于上下文
调试技巧
- 使用performance.mark跟踪任务
performance.mark('start-task');
// 执行一些操作
performance.mark('end-task');
performance.measure('task-duration', 'start-task', 'end-task');
console.log(performance.getEntriesByName('task-duration'));
- 监控事件循环延迟
let last = Date.now();
function monitor() {
const now = Date.now();
const delay = now - last - 1000; // 预期1秒间隔
console.log('Event loop delay:', delay);
last = now;
setTimeout(monitor, 1000);
}
monitor();
- 使用Chrome DevTools的Performance面板
- 记录运行时性能
- 分析任务分布和长任务
- 查看主线程活动