JavaScript 是单线程运行的语言,同一时间只能执行一个任务。单线程意味着:
如果某个任务执行时间过长,后续任务会被阻塞。
同步任务和异步任务的调度需要一种机制来管理。
为了解决这个问题,事件循环应运而生,它可以将任务分为两类:
同步任务:立即执行,直接进入主线程。
异步任务:由浏览器或Node.js的线程处理,待完成后加入到任务队列,等待主线程执行。
一、为什么需要事件循环?
JavaScript 是单线程运行的语言,同一时间只能执行一个任务。单线程意味着:
如果某个任务执行时间过长,后续任务会被阻塞。
同步任务和异步任务的调度需要一种机制来管理。
为了解决这个问题,事件循环应运而生,它可以将任务分为两类:
同步任务:立即执行,直接进入主线程。
异步任务:由浏览器或Node.js的线程处理,待完成后加入到任务队列,等待主线程执行。
二、事件循环的基本概念
执行栈(Call Stack)
JavaScript 执行代码时,会将需要执行的函数压入栈中,执行完后再弹出。
任务队列(Task Queue)
当异步任务完成时,其回调函数会被加入任务队列中,等待主线程空闲时执行。
宏任务(Macro Task)和微任务(Micro Task)
宏任务:包括 setTimeout、setInterval、setImmediate、I/O、UI 渲染等。
微任务:包括 Promise.then、MutationObserver 等。
微任务的优先级高于宏任务,即主线程会先清空所有微任务队列,再处理宏任务。
三、事件循环的执行过程
执行主线程上的同步代码,直到执行栈为空。
检查微任务队列,依次执行其中的所有任务。
执行一个宏任务(例如 setTimeout 回调),然后再检查微任务队列。
重复以上步骤,直到所有任务执行完毕。
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
最终输出顺序:
script start
script end
promise1
promise2
setTimeout
最佳实践:
避免阻塞主线程
长时间的计算任务应切分为小任务,可以使用 setTimeout 或 Web Worker 分块执行。
合理使用微任务
微任务会在主线程空闲前执行,使用不当可能导致 UI 渲染延迟。
优化异步任务的调度
根据优先级选择适合的任务类型(宏任务或微任务)。
了解浏览器性能瓶颈
使用工具如 Chrome DevTools 分析性能,避免过多的异步操作。