事件循环(Event Loop)
引入背景
我们都知道,js的工作是操作dom元素并响应交互,例如点击按钮弹出界面,输入文字记录信息等等。这决定了js必须是单线程的。
用通俗易懂的话来说,就是js在同一时间只能做一件事情。
这会导致一些问题。如果碰到耗时操作(如网络请求、定时器),单线程会阻塞后续代码执行,导致页面卡死。
为了解决这个问题,事件循环机制应运而生。
事件循环工作流程
- 当网页打开时,浏览器加载了script中的代码,(或者node.js执行一个js文件时)整个脚本代码会被包装成一个宏任务
这个宏任务被放到了宏任务队列。
- 事件循环启动。取出第一个宏任务,执行里面的代码。同步代码压入调用栈,立刻执行,执行完弹出。异步代码会被挂起,交给浏览器或者其他线程处理,将来回调函数会被放在队列。具体放到哪里,取决于异步操作的类型:
- 宏任务源:比如
setTimeout,setInterval等,等当定时时间到了,这些会被放在宏任务队列。 - 微任务源:比如
Promise.then/catch/finally等,他们的回调会放到微任务队列里。
- 宏任务源:比如
async/await 是promise的语法糖,await后面的代码等价于.then()会被放进微任务队列。
调用栈空了,这个宏任务结束了,此时,去清空微任务队列。
微任务队列清空之后,若为浏览器环境,浏览器可能会去渲染。是否需要重新渲染页面等等。
然后再取一个宏任务,开始新的一轮----------
工作流程例子
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');首先,整个 <script> 代码作为第一个宏任务(记为宏任务 0)进入宏任务队列,事件循环启动。
1. 执行宏任务 0:
- 同步代码
console.log('1')压入调用栈,立刻执行,输出1。 - 遇到
setTimeout,交给浏览器的定时器线程计时 0ms,计时结束后将其回调推入宏任务队列,记为宏任务 A。 - 遇到
Promise.resolve().then(...),将其回调推入微任务队列,记为微任务 A。 - 同步代码
console.log('6')压入调用栈,立刻执行,输出6。
2. 宏任务 0 结束,清空微任务队列:
- 执行微任务 A:输出
4。 - 微任务 A 中又遇到
setTimeout,交给定时器线程,0ms 后回调推入宏任务队列,记为宏任务 B。 - 微任务队列清空。
3. 取下一个宏任务 A:
- 执行
console.log('2'),输出2。 - 遇到
Promise.resolve().then(...),将其回调推入微任务队列,记为微任务 B。
4. 宏任务 A 结束,清空微任务队列:
- 执行微任务 B:输出
3。 - 微任务队列清空。
5. 取下一个宏任务 B:
- 执行
console.log('5'),输出5。 - 无新任务产生,结束。
最终输出顺序:1 → 6 → 4 → 2 → 3 → 5
