浏览器事件循环

事件循环

JS是单线程的,在运行js代码的时候有可能修改真实的dom,如果不是单线程就乱套了。所有js在执行的过程中是单线程的。单线程就意味着会遇到阻塞的问题,当一段代码执行复杂,或者ajax请求都会阻塞js代码执行。因此js又分为同步和异步。同步方法一旦调用就立马执行,异步就放到任务队列中,当执行栈执行完毕之后就会轮询任务队列,把任务队列的首部拿到执行栈中执行,如此循环就称为浏览器事件循环。

任务队列

任务队列又分为宏任务和微任务
宏任务: setTimeout/setInterval/setImmediate/UI rendering/script/IO
微任务: process.nextTick, Promise, MutationObserver,async / await

每一次事件循环触发时:
执行宏任务(script)
然后执行宏任务过程中产生的微任务
如果微任务中又产生微任务,则继续执行微任务,直到微任务执行完毕
微任务执行完毕之后,继续执行宏任务,开启下一轮事件循环

async / await 执行顺序

  1. await asyncFunc();
  2. 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

  1. 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 回调

image.png
图中每个框被成为事件循环机制的一个阶段,每个阶段都有一个 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 队列为空,会有两件事发生
    • 如果 setImmediate 回调需要执行,poll 阶段将会停止并进入 check 阶段执行回调
    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置,防止一直等待下去,一段时间后自动进入 check 阶段

      check

      check 阶段,这是一个比较简单的阶段,直接执行 setImmediate 的回调

      process.nextTick

      process.nextTick 是独立于事件循环的任务队列
      在每一个事件循环阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。