浏览器事件循环
事件循环
JS是单线程的,在运行js代码的时候有可能修改真实的dom,如果不是单线程就乱套了。所有js在执行的过程中是单线程的。单线程就意味着会遇到阻塞的问题,当一段代码执行复杂,或者ajax请求都会阻塞js代码执行。因此js又分为同步和异步。同步方法一旦调用就立马执行,异步就放到任务队列中,当执行栈执行完毕之后就会轮询任务队列,把任务队列的首部拿到执行栈中执行,如此循环就称为浏览器事件循环。
任务队列
任务队列又分为宏任务和微任务
宏任务: setTimeout/setInterval/setImmediate/UI rendering/script/IO
微任务: process.nextTick, Promise, MutationObserver,async / await
每一次事件循环触发时:
执行宏任务(script)
然后执行宏任务过程中产生的微任务
如果微任务中又产生微任务,则继续执行微任务,直到微任务执行完毕
微任务执行完毕之后,继续执行宏任务,开启下一轮事件循环
async / await 执行顺序
await asyncFunc();
console.log("a");
async/await执行顺序是这样的:
- await后面的asyncFunc就相当于promise的excutor,会被立马执行,
- await下面的语句会被注册为一个微任务。
例子
eg1: ``` console.log(‘script start’)
async function async1() { await async2() console.log(‘async1 end’) }
async function async2() { console.log(‘async2 end’) } async1()
setTimeout(function() { console.log(‘setTimeout’) }, 0)
new Promise(resolve => { console.log(‘Promise’) resolve() }).then(function() { console.log(‘promise1’) }).then(function() { console.log(‘promise2’) })
console.log(‘script end’)
//script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
eg2:
console.log(‘script start’)
async function async1() { await async2() console.log(‘async1 end’) } async function async2() { console.log(‘async2 end’) return Promise.resolve().then(()=>{ console.log(‘async2 end1’) }) } async1()
setTimeout(function() { console.log(‘setTimeout’) }, 0)
new Promise(resolve => { console.log(‘Promise’) resolve() }).then(function() { console.log(‘promise1’) }).then(function() { console.log(‘promise2’) })
console.log(‘script end’)
// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout ```
分析
eg1: 如果await后面跟着一个同步变量或者函数,await 1; 这种情况会把await下面的代码注册为一个微任务,然后跳出函数去执行其他函数
eg2: 如果await后面跟着异步的函数或者primise,这种情况下,并不会把await下面的代码注册成一个微任务。而是跳出函数继续执行其他同步代码,最后再把刚刚await下面的代码添加微任务队列。注意此时的微任务队列是注册了其他的微任务了
结论
如果await后面是同步,则把await后面的代码会立马执行,await下面的代码会注册为一个微任务,放到微任务对尾。
如果await后面是异步,则立马执行await后面的代码,然后跳出该函数,继续执行其他代码,在事件轮询之前,把刚刚await后面的代码推到微任务队列中。(注意,这个时候的微任务队列可能已经注册了其他的微任务了)
Node事件循环
事件循环
Node也是有事件循环的。Node的事件循环是基于非阻塞I/O操作的。
宏任务和微任务
宏任务:
- setTimeout
- setInterval
- setImmediate
- script(整体代码)
- I / O 操作
微任务:
- process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
- Promise.then 回调
图中每个框被成为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但是通常情况下,当事件循环进入特定的阶段时,它将执行特性该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。
因此,上图可以简化为以下流程:
- 输入数据阶段(incoming data)
- 轮询阶段(poll)
- 检查阶段(check)
- 关闭时间回调阶段(close callback)
- 定时器检测阶段(timers)
- I / O 事件回调阶段(I / O callbacks)
- 闲置阶段(idle,prepare)
-
阶段概述
定时器检测阶段(timers):本阶段执行 timers 的回调,即 setTimeout、setInterval 里面的回调函数
- I / O 事件回调阶段(I / O callbacks):执行延迟到下一个循环迭代的 I / O 回调,即上一轮循环中未被执行的一些 I / O 回调
- 闲置阶段(idle,prepare):仅供内部使用
- 轮询阶段(poll):检索新的 I / O 事件;执行与 I / O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些计时器和 setImmediate 调度之外),其余情况 node 将在适当的时候在此阻塞
- 检查阶段(check):setImmediate 回调函数将在此阶段执行
- 关闭事件回调阶段(close callback):一些关闭的回调函数,如
socket.on('close', ...)
三大重点阶段
日常开发中绝大部分异步任务都在 poll、check、timers 这三个阶段处理,所以需要重点了解这三个阶段
timers
timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只是尽快执行。
poll
poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下:
如果当前已经存在定时器,而且有定期到时间了,拿出来执行,事件循环将会到 timers 阶段
如果没有定时器,回去看回调函数队列
- 如果 poll 队列不为空,会遍历回到队列并同步执行,直到队列为空或达到系统限制
- 如果 poll 队列为空,会有两件事发生