# 事件循环(EventLoop)
有没有想过,为什么有时候SetTimeout定时不准确?
更新时间:2019-04-24
# 异步队列的种类
事件循环有 2 种异步队列: 宏任务队列 和 微任务队列。
其中,对于 “宏任务队列”,是 一个一个 执行;对于 “微任务队列”,是 一队一队 执行。
# 宏任务(Macro Task)
常见的有:script(整体代码)、setTimeout、setInterval、setImmediate、Ajax回调、I/O操作、DOM事件监听
宏任务由 事件触发线程 维护。
# 微任务(Micro Task)
常见的有:Promise.then、catch、finally
微任务队列由 JS引擎线程 维护。
# 执行栈
执行栈 是一个 存储函数调用的栈结构,遵循 先进后出(FILO) 的原则。
栈内的每个 栈帧 代表一个函数的 “执行上下文”
“执行上下文” 包含:这个函数的作用域、上层作用域的指向、函数的参数、函数中声明的变量等。
# 事件循环步骤
# EventLoop
解释如下:
- 1、JS引擎 首先会把 主代码块 放入
宏任务队列
(此时宏任务队列
只有一个任务,那就是主代码块) - 2、从
宏任务队列
中取出一个宏任务
- 3、从上往下执行。
- 如果遇到
同步代码
,直接执行、输出 - 如果遇到setTimeout之类的
宏任务
,将其进宏任务队列
- 如果遇到Promise.then之类的
微任务
,将其送进微任务队列
- 如果遇到
- 4、若 当前执行栈 为空,JS引擎 会去检查
微任务队列
是否有任务在排队 - 5、执行
微任务队列
中的微任务
(先进先出) - 6、
微任务队列中
的微任务
全部执行完毕,本轮事件循环结束 - 7、回到第2步,检查
宏任务队列
中是否有未执行的宏任务
,继续下一轮循环
总结: 先执行宏任务,然后执行该宏任务产生的微任务。微任务执行完毕后,回到宏任务进行下一轮循环。
注意:
Ajax请求完毕后
触发的回调函数会进入宏任务队列
参考链接:JavaScript的Event Loop机制 (opens new window)
# 示例
下面这两道题,会输出什么?
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
console.log(3)
Promise.resolve().then(() => {
console.log(4)
}).then(() => {
console.log(5)
})
}).then(() => {
console.log(6)
})
Promise.resolve().then(() => {
console.log(7)
}).then(() => {
console.log(8)
})
console.log(9)
// 1 9 3 7 4 6 8 5 2
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
async function async1() {
console.log('a')
await async2()
console.log('b')
}
async function async2() {
console.log('c')
}
console.log('d')
setTimeout(function () {
console.log('e')
}, 0)
async1()
new Promise(function (resolve) {
console.log('f')
resolve()
}).then(function () {
console.log('g')
})
console.log('h')
// d a c f h b g e
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
总结:
await紧跟
的Promise相当于new Promise,会立即执行await下面
的代码,相当于promise.then(代码),视作微任务Promise内代码执行完
、Promise.resolve()
,都会开始执行promise.then,视作微任务- setTimeout属于宏任务(由 事件触发线程 维护)
- 要注意Promise的状态变更时机
# Node事件循环
事件循环是 node 处理非阻塞I/O 的机制。
Node中的事件循环有6个阶段:
timers:执行
setTimeout
、setInterval
回调I/O callbacks:处理上一轮循环中 少量 未执行的 I/O 回调
idle、prepare:内部使用
poll:检索新的I/O事件、 执行 I/O 回调
check:执行
setImmediate
回调close callbacks:执行一些关闭回调(如:socket的
close
回调)
事件循环的阶段顺序:
输入数据
-> 轮询(poll)
-> 检查(check)
-> 关闭事件回调(close callback)
-> 定时器检测(timers)
-> I/O事件回调(I/O callbacks)
-> 闲置阶段(idle prepare)
-> 轮询(poll)
日常开发中绝大部分异步任务都是在 poll、check、timers 这 3 个阶段处理。
# poll
poll是个至关重要的阶段:
- 是否存在定时器,且时间到了?
- 是:timers
- 是否有回调函数?
- 是:拿出队列中的方法依次执行
- setImmediate回调是否需要执行?
- 是:进入check
- 否,则一直poll。等待回调被加入到队列中,并立即执行回调
# process.nextTick
独立于 事件循环 的 任务队列。
执行时机:每个阶段执行完之后、并且 微任务队列 执行前
# 与 浏览器事件循环 的不同
- 浏览器端,“微任务” 在 “本轮事件循环结束前” 执行完
- Node 11 之前,
nextTick
、微任务
在 事件循环的各个阶段之间 执行 - Node 11 之后,
nextTick
是微任务
的一种。都是在每个宏任务执行完后,会执行该宏任务对应的微任务(与 “浏览器事件循环” 类似)