事件循环的理解
事件循环的理解
事件循环的理解
是什么
首先,JavaScript是一门单线程的语言,意味着_同一时间内只能做一件事_,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环
首先理解
JS是单线程语言,分为 同步任务 和 异步任务
- 同步任务:立即执行的任务;在主线程上排队执行,形成一个执行栈;只有前一个任务执行完毕,才能继续执行下一个任务。
- 异步任务:异步执行的任务:不进入主线程,而进入“任务队列”的任务;只有等主线程任务全部执行完毕。“任务队列”的任务才会进入主线程执行。比如
ajax网络请求,setTimeout定时函数等 - 微任务:较短时间内可以完成的任务
- 宏任务:需要相对较长时间才能完成的任务
| 微任务(microtask) | 宏任务(macrotask) | |
|---|---|---|
| 谁发起的 | JS引擎 | 宿主(Node、浏览器) |
| 具体事件 | Promise.then()/.catch()、async await、MutaionObserver、nextTick(Node.js 环境) | script(整体代码)、setTimeout/setInterval 、UI渲染任务、事件处理器、I/O操作(如文件读取)、Ajax网络请求等异步任务 |
| 执行顺序 | 先执行 | 后执行 |
| 会触发新一轮事件循环吗 | 不会 | 会 |
- 栈:是一种后进先出的数据结构,数据元素在插入(即进栈)和删除(即出栈)时均从栈顶进行操作。
类似于堆在一起的餐盘,最先放的盘子在最底下,最后放的盘子在最上面,需要把最上面的盘子一个个拿走,才能拿到最下面的盘子。
- 队列:是一种先进先出的数据结构,数据元素在队尾插入而从队首删除的。
类似于我们去排队买东西,先去的同学可以先买到。
同步任务与异步任务的运行流程图如下:
从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕后,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环
二、宏任务与微任务
如果将任务划分为同步任务和异步任务并不是那么的准确,举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
如果按照上面流程图来分析代码,我们会得到下面的执行步骤:
console.log(1),同步任务,主线程中执行setTimeout(),异步任务,放到Event Table,0 毫秒后console.log(2)回调推入Event Queue中new Promise,同步任务,主线程直接执行.then,异步任务,放到Event Tableconsole.log(3),同步任务,主线程执行
所以按照分析,它的结果应该是 1 => 'new Promise' => 3 => 2 => 'then'
但是实际结果是:1=>'new Promise'=> 3 => 'then' => 2
出现分歧的原因在于异步任务执行顺序,事件队列其实是一个“先进先出”的数据结构,排在前面的事件会优先被主线程读取
例子中 setTimeout回调事件是先进入队列中的,按理说应该先于 .then 中的执行,但是结果却偏偏相反
原因在于异步任务还可以细分为微任务与宏任务
这时候,事件循环,宏任务,微任务的关系如图所示
按照这个流程,它的执行机制是:
- 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
- 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完
回到上面的题目
1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
流程如下
- 遇到 console.log(1) ,直接打印 1
- 遇到定时器,属于新的宏任务,留着后面执行
- 遇到 new Promise,这个是直接执行的,打印 ‘new Promise’
- .then 属于微任务,放入微任务队列,后面再执行
- 遇到 console.log(3) 直接打印 3
- 好了本轮宏任务执行完毕,现在去微任务列表查看是否有微任务,发现 .then 的回调,执行它,打印 ‘then’
- 当一次宏任务执行完,再去执行新的宏任务,这里就剩一个定时器的宏任务了,执行它,打印 2
三、async与await
async 是异步的意思,await则可以理解为 async wait。所以可以理解async就是用来声明一个异步方法,而 await是用来等待异步方法执行
async
async函数返回一个promise对象,下面两种方法是等效的
1
2
3
4
5
6
7
8
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await
正常情况下,await命令后面是一个 Promise对象,返回该对象的结果。如果不是 Promise对象,就直接返回对应的值
1
2
3
4
5
6
async function f(){
// 等同于
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
不管await后面跟着的是什么,await都会阻塞后面的代码
1
2
3
4
5
6
7
8
9
10
11
12
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
上面的例子中,await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码
所以上述输出结果为:1,fn2,3,2
四、流程分析
通过对上面的了解,我们对JavaScript对各种场景的执行顺序有了大致的了解
这里直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
// script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout
分析过程:
- 执行整段代码,遇到
console.log('script start')直接打印结果,输出script start - 遇到定时器了,它是宏任务,先放着不执行
- 遇到
async1(),执行async1函数,先打印async1 start,下面遇到await怎么办?先执行async2,打印async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码 - 跳到
new Promise这里,直接执行,打印promise1,下面遇到.then(),它是微任务,放到微任务列表等待执行 - 最后一行直接打印
script end,现在同步代码执行完了,开始执行微任务,即await下面的代码,打印async1 end - 继续执行下一个微任务,即执行
then的回调,打印promise2 - 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印
settimeout
所以最后的结果是:script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout


