Appearance
事件循环
js 是单线程的, 为了高效运行就需要通过事件循环和任务队列机制,高效地处理异步事件。
事件循环机制
在事件循环中,当主线程执行完同步任务后,会检查事件队列中是否有待处理事件, 如果有会取出事件并执行,这个过程就是事件循环, 由主线程和任务队列两部分组成,主线程处理同步任务,任务队列负责处理异步任务。
常见的异步任务为:
- 回调函数 callback
- Promise/async await
- Generator
- 事件监听
- 发布/订阅
- 计时器
- requestAnimationFrame
- MutationObserver
- process.nextTick
- I/O操作
在事件循环的过程中,每次主线程的同步任务执行完毕后,就开始执行任务队列中的异步任务
任务队列
异步任务分为宏任务和微任务
宏任务
- script(整体代码)
- setTimeout
- setInterval
- setImmediate
- I/O
- UI render
微任务
- process.nextTick
- Promise
- Async/Await(实际就是promise)
- MutationObserver(html5新特性)
任务队列的执行顺序为: 执行宏任务,然后执行宏任务产生的为任务,如果微任务产生了新的微任务,就继续执行微任务。微任务执行玩在回到宏任务进行下一轮循环。
async/await执行顺序
await 后面的函数执行完毕后会产生一个 promise.then 的为任务。执行完await之后,分两种情况:
- 如果 await 后面跟的是一个同步任务时,会把下面的代码注册成微任务(类似于用 promise.then 包裹)直接跳出async函数,执行其他代码 。
- 如果 await 后面跟的是异步任务时, 会先跳出 async 函数,执行其他宏任务,然后再把 await 下面的逻辑注册为微任务执行。
js
console.log('1')
async function async1() {
await async2()
console.log('2')
}
async function async2() {
console.log('3')
}
async1()
new Promise(resolve => {
console.log('4')
resolve()
})
.then(function() {
console.log('5')
})
.then(function() {
console.log('6')
})
console.log('7')
await 后面是同步任务时, 执行完 await 后面的任务时, 直接把下面的任务注册成微任务。执行顺序为 1、 3 、 4 、 7 、 2 、 5 、 6
js
console.log('1')
async function async1() {
await async2()
console.log('2')
}
async function async2() {
console.log('3')
return Promise.resolve().then(()=>{
console.log('8')
})
}
async1()
new Promise(resolve => {
console.log('4')
resolve()
})
.then(function() {
console.log('5')
})
.then(function() {
console.log('6')
})
console.log('7')
await 后面是异步任务时, 执行完 await 后面的任务时, 先跳出 async 函数执行其他任务,再把 await 下面的代码包装成微任务执行 s。执行顺序为 1、 3 、 4、 7 、 8 、 5 、 6 、 2
node 中的事件循环
在 node环境中, V8 引擎作为 js 代码的解析器,V8 引擎将代码解析后调用对应的 node api, 而这些 api 底层使用 libv 引擎驱动,执行对应的任务,并把不同的任务放到不同的任务队列中,等待执行。因此node 中的时间循环存在于 libv 中
node 的事件循环模型
流程: libv 在接收到外部的任务时
- timer: 定时器检测阶段, 这个阶段执行 setTimeout(callback) 和 setInterval(callback) 预定的 callback;
- I/O callback: I/O 事件回调阶段, 此阶段执行某些系统操作的回调,例如TCP错误的类型。 例如,如果TCP套接字在尝试连接时收到 ECONNREFUSED,则某些* nix系统希望等待报告错误。 这将操作将等待在==I/O回调阶段==执行;
- idel prepare: 闲置阶段, 仅在内部使用, 阶段: 仅node内部使用;
- poll: 轮询阶段
- check: 检查阶段, setImmediate() 的回调会在这个阶段执行
- close: callback 关闭回调阶段, 比如关闭 socket 等
poll 是一个至关重要的阶段,系统会做两件事:
执行下限时间已经达到的timers的回调
执行 I/O 操作 并且在进入该阶段如果没有设置 timer 的话,会发生以下两件事
- 如果 poll 队列不为空,会遍历回调队列并同步执行,知道 poll 队列为空或者打到系统限制
- 如果队列为空会发生两件事:
- 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进行到 check 阶段执行回调。
- 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有一个超时设置防止一直等待下去。
当设定了 timer 并且 poll 队列为空,会判断是否有 timer 超时,如果有会到 timer 阶段执行回调。
process.nextTick
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
整个过程自己的理解:
- timer 执行执行时间, 这个时候并不执行回调, 只是放入到 timer 的队列
- 执行 I/O 操作, 把回调放入 I/O callbacks 阶段的队列
- poll 开始分配, 轮询监听事件
- 执行 setImmediate 的回调
- 执行 callback 关闭回调
每个队列都可能包含自己的微任务,在每个队列执行为任务前,会先执行 process.nextTick
Node与浏览器的 Event Loop 差异
浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。 而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。