Event Loop事件循环
1. 同步和异步
JavaScript是一种单线程语言,所有任务都在一个线程上完成。一旦遇到大量任务或者遇到一个耗时的任务,比如加载一个高清图片,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。
同步:调用一旦开始,调用者必须等待方法执行完成,才能继续执行后续方法。
异步:方法一旦开始,立即返回,调用者无需等待其中方法执行完成,就可以继续执行后续方法。
2. 事件循环机制 Event Loop
事件循环(Event Loop)是 JavaScript 运行时(浏览器/Node.js)用于协调同步代码、异步回调、页面渲染的一套调度机制。它决定了代码执行的顺序和时机
2.1 执行顺序
- 先执行一个宏任务也就是整体script里的代码
- 遇到同步任务立即执行
- 遇到宏任务则将宏任务放入宏任务队列中
- 遇到微任务则将微任务放入微任务队列中
- 当所有同步代码执行完毕后,再将微任务从队列中调入主线程执行直至微任务队列清空(哪怕执行微任务时又产生了新的微任务,也会在本轮继续清空,直到微任务队列为空,才会取下一个宏任务。)
- 检查是否需要渲染页面,如果需要渲染 → 执行渲染流程(样式计算 → 布局 → 绘制);如果不需要 → 跳过
- 再从宏任务队列中取出一个执行
- 一直循环直至所有任务执行完毕
2.2 宏任务
| 宏任务来源 | 说明 | 示例 |
|---|---|---|
| 整个script标签的执行 | 事件循环启动后取出的第一个宏任务,所有全局同步代码都是它的执行体 | 页面加载时执行的JS代码 |
| setTimeout/setInterval回调 | 定时器延迟时间达到后,回调被加入宏任务队列 | setTimeout(()=>{}, 0) |
| UI交互事件回调 | 用户操作触发后,回调加入宏任务队列 | click/keydown/mousemove/touchstart等 |
| I/O完成回调 | 浏览器底层I/O操作完成后触发的回调(注意:不是JS回调本身) | fetch/XHR网络请求完成、FileReader文件读取完成、IndexedDB操作完成 |
| postMessage回调 | 跨窗口/跨Worker通信时,消息被接收后触发的回调 | window.postMessage(data)的onmessage回调 |
| MessageChannel回调 | 通过MessageChannel创建的端口发送消息后,接收端的onmessage回调 | const channel = new MessageChannel(); channel.port1.onmessage = () => {} |
| requestAnimationFrame回调 | 浏览器下一次渲染前执行的回调,属于宏任务,但优先级高于普通宏任务 | 做动画时常用的API |
| requestIdleCallback回调 | 浏览器空闲时期执行的回调,优先级低于普通宏任务,只有主线程无任务时才会执行 | 执行低优先级任务(如日志上报、预加载) |
2.2 微任务
| 微任务来源 | 说明 | 你熟悉的场景 |
|---|---|---|
| Promise.prototype.then/catch/finally回调 | 最常见的微任务,Promise状态变更后触发 | 你写异步逻辑最常用的API |
| await后面的代码 | await xx等价于 Promise.resolve(xx).then(()=>{}),await后的代码就是then的回调,属于微任务 | await后面的语句不会阻塞同步代码 |
| queueMicrotask回调 | ES2020新增的API,专门用于创建微任务,比Promise.then更纯粹(没有Promise状态转换的开销) | Vue3的nextTick底层就用它兜底 |
| MutationObserver回调 | 监听DOM变化触发的回调,DOM变更后、下一个宏任务前执行 | Vue2的nextTick在兼容环境下用它实现 |
| IntersectionObserver回调 | 监听元素可见性变化触发的回调,属于微任务 | 图片懒加载、曝光埋点的核心API |
2.4 例题
setTimeout(function(){ console.log('1'); }); new Promise(function(resolve){ console.log('2'); resolve(); }).then(function(){ console.log('3'); }); console.log('4'); //输出顺序:2 4 3 1解释:
- 遇到setTimout,异步宏任务,放入宏任务队列中
- 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
- 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到同步任务console.log(‘4’);输出4;主线程中同步任务执行完
- 从微任务队列中取出任务到主线程中,输出3,微任务队列为空
- 从宏任务队列中取出任务到主线程中,输出1,宏任务队列为空,结束
setTimeout(()=>{ new Promise(resolve =>{ resolve(); }).then(()=>{ console.log('test'); }); console.log(4); }); new Promise(resolve => { resolve(); console.log(1) }).then( () => { console.log(3); Promise.resolve().then(() => { console.log('before timeout'); }).then(() => { Promise.resolve().then(() => { console.log('also before timeout') }) }) }) console.log(2); //输出:1 2 3 before timeout also before timeout 4 test解释:
- 遇到setTimeout,异步宏任务,将() => {console.log(4)}放入宏任务队列中
- 遇到new Promise,Promise在实例化的过程中所执行的代码都是同步进行的,所以输出1;
- 而Promise.then中注册的回调才是异步执行的,将其放入微任务队列中
- 遇到同步任务console.log(2),输出2;主线程中同步任务执行完
- 从微任务队列中取出任务到主线程中,输出3,此微任务中又有微任务,Promise.resolve().then(微任务a).then(微任务b),将其依次放入微任务队列中;
- 从微任务队列中取出任务a到主线程中,输出 before timeout;
- 从微任务队列中取出任务b到主线程中,任务b又注册了一个微任务c,放入微任务队列中;
- 从微任务队列中取出任务c到主线程中,输出 also before timeout;微任务队列为空
- 从宏任务队列中取出任务到主线程,此任务中注册了一个微任务d,将其放入微任务队列中,接下来遇到输出4,宏任务队列为空
- 从微任务队列中取出任务d到主线程 ,输出test,微任务队列为空,结束
