EventLoop浏览器/宏任务/微任务概念及示例
最近看了很多关于浏览器EventLoop的文章,学习了很多,这里将这些进行一个简单的总结:
一、概念
1.什么是EventLoop:
Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for
and dispatches events or messages in a program.)简单来说就是计算机系统的一种运行机制
2.什么是宏任务/微任务:
在ES6 规范中是这样定义的,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。
1) 宏任务有哪些:
- script的全部代码
- setTimeout
- setInterval
- setImmediate
- I/O
- UI-Render
- postMessage
- MessageChannel
(对于普通的使用我们大部分关注和注意的应该是Script全部代码、setTimeout、setInterval)
2) 微任务有哪些:
- Process.nextTick(node.js独有)
- Promise
- MutaionObserver
- 以Promise为基础开发的其它技术(async/await等)
二、执行顺序
- 首先我们都知道JS是一个单线程的运行机制,在同步操作的情况下我们的代码是一个从上往下的运行过程.上面写到了Script的代码是宏任务,那么我们代码的执行一开始可视作一个宏任务的执行,而每一个宏任务里都定义了一个微任务的队列,每当当前宏任务执行完成之后便会去检查微任务队列里是否有微任务,如果有,就执行,直到微任务队列执行完成,这时便回到宏任务队列去执行下一个宏任务,如此循环便是浏览器的EventLoop
- 宏任务队列执行当前宏任务 --> 当前宏任务执行完成查找当前宏任务中微任务队列 --> 微任务队列执行完成(微任务队列为空) --> 在宏任务队列执行下一个宏任务
- 以上便是简单理解浏览器的EventLoop
三、示例解析
A. 首先看一个基础示例
console.log('script-start')
setTimeout(() => {
console.log('settimeout');
});
console.log('script-end');
//打印顺序为 script-start
// script-end
// settimeout
分析: 首先进入代码执行,外层同步代码为第一个宏任务执行,从上往下
- 执行 console.log(‘script-start’) 打印第一个log script-start
- 遇到定时器setTimeout,属于宏任务,将定时器里的任务放到宏任务队列
- 继续往下执行 console.log(‘script-end’) 打印第二个log script-end
- 此时宏任务执行完成,查找微任务队列为空,则回到宏任务队列查找下一个宏任务,为刚刚碰到的定时器setTimeout
- 执行宏任务setTimeout console.log(‘settimeout’) 打印第三个log settimeout
B. 增加微任务promise的示例
console.log('script-start')
setTimeout(() => {
console.log('settimeout');
});
new Promise((resolve, reject) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise1-then');
});
console.log('script-end');
//打印顺序为 script-start
// promise1
// script-end
// promise1-then
// settimeout
分析: 首先进入代码执行,外层同步代码为第一个宏任务执行,从上往下
- 执行 console.log(‘script-start’) 打印第一个log script-start
- 遇到定时器setTimeout,属于宏任务,将定时器里的任务放到宏任务队列
- 遇到Promise,首先都知道Promise创建便执行,于是执行console.log(‘promise1’) 打印第二个log promise1
- 由于Promise的回调是微任务,所以将Promise的回调也就是then中的代码放到微任务队列
- 继续往下执行 console.log(‘script-end’) 打印第三个log script-end
- 此时宏任务执行完成,查找微任务队列,发现刚刚放进微任务的promise的回调
- 执行promise.then中的代码 执行console.log(‘promise1-then’) 打印第四个log promise1-then
- 再查找微任务队列,为空了,则回到宏任务队列查找下一个宏任务,为刚刚碰到的定时器setTimeout
- 执行宏任务setTimeout console.log(‘settimeout’) 打印第五个log settimeout
C. 增加async/await的示例
console.log('script-start')
async function a() {
console.log('a-start');
await b();
console.log('a-end');
}
async function b() {
console.log('b');
}
a()
setTimeout(() => {
console.log('settimeout');
});
new Promise((resolve, reject) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise1-then');
});
console.log('script-end');
//打印顺序为 script-start
// a-start
// b
// promise1
// script-end
// a-end
// promise1-then
// settimeout
分析: 首先进入代码执行,外层同步代码为第一个宏任务执行,从上往下
- 执行 console.log(‘script-start’) 打印第一个log script-start
- 碰到async a 以及 async b的函数声明,但是没有执行,所以继续往下
- 遇到a(),执行async a进入a函数执行,遇到console.log(‘a-start’) 打印第二个log a-start
- 碰到await b() 先进入b函数执行, 遇到console.log(‘b’) 打印第三个log b
- b()执行完成回到a()函数中,这时候await下面还有语句,怎么办呢? 答案是将下面的语句作为一个微任务,放进微任务队列里
- 然后跳出a()函数 继续往下执行
- 遇到定时器setTimeout,属于宏任务,将定时器里的任务放到宏任务队列
- 遇到Promise,执行console.log(‘promise1’) 打印第四个log promise1
- 将Promise的回调也就是then中的代码放到微任务队列
- 继续往下执行 console.log(‘script-end’) 打印第五个log script-end
- 此时宏任务执行完成,查找微任务队列,此时微任务队列里第一个微任务便是我们放进去的async里await下面的代码,也就是 console.log(‘a-end’) 执行打印第六个log a-end
- 再继续执行微任务,也就是执行promise.then中的代码 执行console.log(‘promise1-then’) 打印第七个log promise1-then
- 再查找微任务队列,为空了,则回到宏任务队列查找下一个宏任务,为刚刚碰到的定时器setTimeout
- 执行宏任务setTimeout console.log(‘settimeout’) 打印第八个log settimeout
重点:
- 可能在这里大家就比较疑惑了,async/await的执行到底是怎么样的?
- 这里简单的解释一下,我理解的async/await实际是Promise的一个语法糖,或者说是以Promise为基础的新技术吧,async函数中的await实际类似于Promise中的.then,首先因为代码是从右往左执行的,所以当执行到await这一行的时候,应该先从右边执行,执行完成了之后,碰到await,而await又相当于是一个Promise的.then效果,所以将接下来要执行的下面的代码作为一个微任务放到了微任务队列中
也就是说async/await函数中:
- 从上往下执行 --> 碰到await语句先执行右侧代码 --> 右侧代码执行完成之后将await左侧代码及下面代码放进微任务队列
–> 跳出async函数执行外层同步代码