浏览器和 node 中的 Event Loop

前言

众所周知,JavaScript 这门语言是单线程的。那也就是说,JavaScript 在同一时间只能作一件事,后面的事情必须等前面的事情作完以后才能获得执行。javascript

任务队列

JavaScript 单线程这件事乍一看好像没毛病,代码原本就是须要按顺序执行的嘛,先来后到,后面的你就先等着。若是是计算量致使的排队,那没办法,老老实实排吧。但若是是由于 I/O 很慢(好比发一个 Ajax 请求,须要 200ms 才能返回结果),那这个等待时间就没太必要了,彻底能够先执行后面其余的任务,等你请求的数据回来了再执行 Ajax 后面的操做嘛。html

由此,JavaScript 中的任务分红了两种,第一种是同步任务,指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;第二种是异步任务,指的是不进入主线程、而进入“任务队列”的任务,只有“任务队列”通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。java

其执行过程以下:node

  1. JavaScript 引擎运行 JavaScript 时,有一个主线程和一个任务队列。
  2. 同步任务跑在主线程上面,异步任务扔进任务队列中进行等待。
  3. 主线程中的任务执行完毕以后,回去看看任务队列中有没有异步任务到了须要触发的时机。若是有,那就开始执行异步任务。
  4. 重复的执行主线程的任务和轮询任务队列。

Event Loop

这种主线程不断地从任务队列中读取任务的机制称为 Event Loop(事件循环)。promise

在讲 Event Loop 以前,咱们先来了解一下 macrotask(宏任务)和 microtask(微任务)。浏览器

宏任务

包括 setTimeoutsetIntervalsetImmediate (浏览器仅 IE10 支持)、I/OUI Renderingbash

微任务

包括 process.nextTicknode 独有)、PromiseObject.observe(已废弃)、MutatinObserver异步

这里多说一句,Promise 的执行函数(也就是 new Promise(fn) 中的 fn)是同步任务。socket

浏览器中的 Event Loop

Event Loop 的实如今浏览器和 node 中是不同的,咱们先看浏览器。async

  1. 开始执行主线程的任务
  2. 主线程的任务执行完毕以后去检查 microtask 队列,将已经到了触发时机的任务放进主线程。
  3. 主线程开始执行任务
  4. 主线程的任务执行完毕以后去检查 macrotask 队列,将已经到了触发时机的任务放进主线程。
  5. 主线程开始执行任务
  6. 轮询 microtaskmicrotask

看一下例子

好,讲完了流程,来看下🌰。

console.log('script start'); // 同步任务

setTimeout(function() {
  console.log('setTimeout'); // 放入 宏任务 队列
}, 0);

new Promise((resolve, reject) => {
  console.log('promise'); // 同步任务
  resolve();
})
  .then(function() {
    console.log('promise1'); // 放进 微任务 队列
  })
  .then(function() {
    console.log('promise2'); // 放进 微任务 队列
  });
console.log('script end'); // 同步任务
复制代码

根据上面的标识,先执行同步任务,打印出 “script start” 、 “promise” 、 “script end”,而后开始检查 microtask 队列,打印出 “promise1” 和 “promise2”,而后去检查 macrotask 队列,打印出 “setTimeout”。

这里 setTimeout 虽然它的延迟时间为 0,但它是个宏任务,因此必须等同步任务和微任务执行完毕以后才轮到它。

在看一个🌰。

console.log('script start'); // 同步任务

async function async1() {
  await async2();
  console.log('async1 end'); // 这里就是 then 里面的代码,放入 微任务 队列
}
async function async2() {
  console.log('async2 end'); // 同步任务
}
async1();

setTimeout(function() {
  console.log('setTimeout'); // 放入 宏任务 队列
}, 0);

new Promise((resolve) => {
  console.log('Promise'); // 同步任务
  setTimeout(() => {
    console.log('setTimeout promise'); // 放入 宏任务 队列
    resolve();
  });
})
  .then(function() {
    console.log('promise1'); // 放入 微任务 队列
  })
  .then(function() {
    console.log('promise2'); // 放入 微任务 队列
  });

console.log('script end'); // 同步任务
复制代码

这里的 asyncawait 就是 Promise 的语法糖,要懂得转换,其实上述 asyncawait 代码等价于:

new Promise((resolve) => {
  new Promise((resolve) => {
    console.log('async2 end');
    resolve();
  });
  resolve();
}).then(() => {
  console.log('async1 end');
});
复制代码

因此执行顺序为

script start
async2 end
promise
script end
async1 end
setTimeout
setTimeout promise
promise1
promise2
复制代码

有人可能会有疑惑了,打印 “promise1” 和 “promise2” 是微任务,怎么还晚于 setTimeout 宏任务呢?

虽然它们是微任务,可是因为触发它们的 resolve() 处于 setTimeout 宏任务之中,因此它们实际上是在第二轮微任务的轮询中被触发的。

好了,浏览器的 Event Loop 就说到这个,接下来说一下 nodeEvent Loop

node 中的 Event Loop

node 中的 Event Loop 就比较复杂了,英语好的能够去看官方文档

引用官文档中的一张图,了解一下 Event Loop 的六个阶段。

每一个阶段都有本身的任务队列。

┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
复制代码

6 个阶段

  • timer:执行setTimeoutsetInterval中到期的 callback。

  • pending callback:上一轮循环中少数的 callback 会放在这一阶段执行。

  • idle, prepare:仅在内部使用。

  • poll:最重要的阶段,执行 pending callback,在适当的状况下回阻塞在这个阶段。

  • check:执行setImmediate(setImmediate()是将事件插入到事件队列尾部,主线程和事件队列的函数执行完成以后当即执行setImmediate指定的回调函数)的 callback。

  • close callbacks:执行 close 事件的 callback,例如socket.on('close'[,fn])或者http.server.on('close, fn)

咱们重点关注 timerpollcheck 这三个阶段。

timer

这个阶段执行该阶段任务队列中 setTimeoutsetInterval 到期的回调,这二者须要设置一个时间。按规则来讲,是到了设定的时间以后就应该执行回调,但在实际状况中,回调函数并非一到设定的时间就能获得执行的,有可能被其余的任务阻塞了,须要等其余任务执行完成以后回调才能获得执行。

好比你设定一个 100ms 以后的 setTimeout A 回调,可是在 95ms 时执行了一个其余的 B 任务,须要耗时 10ms,那么在时间来到 100ms 的时候,B 任务还在执行当中,那么此时并不会当即执行 A 回调,而是会再等 5ms,等 B 回调完成以后,而后系统发现 A 回调的触发时机已经到了,那赶忙去执行 A 回调。也就是说在这种状况下,A 回调会在 105ms 的时间被执行。

poll

poll 阶段主要有两个事情要作:

  1. 执行 I/O 回调。
  2. 处理轮询队列中的任务。

当事件循环到达 poll 阶段时,会有下面两种状况:

  1. poll 队列不为空,那就开始执行队列中的任务,直到队列为空或者达到系统限制。
  2. poll 队列为空,那么这种状况又分两种状况;
    1. 若是 check 阶段有 setImmediate 任务须要执行,那么就当即结束当前阶段,转到 check 阶段执行该阶段队列中的回调。
    2. 若是 check 阶段没有 setImmediate 任务须要执行,那么此时会停留在 poll 阶段进行等待,等待有任务进到任务队列中进行执行。

在 2.2 的状况中,还会去检查 timer 阶段有没有任务到了执行时间,若是有,那么转入 timer 阶段执行队列中到期的任务。

check

此阶段会执行 setImmediate 回调,一旦此阶段的任务队列中有了 setImmediate 回调任务,且 poll 阶段的任务执行完了,处于空闲状态,那么就会当即转到 check 阶段执行此阶段任务队列中的任务。

转入此阶段的条件check 任务队列中有了任务,poll 阶段处于闲置状态,或者 poll 阶段等待超时。

setTimeout 和 setImmediate

这二者很类似,也有些不一样。

  • setImmediate设计用于在当前poll阶段完成后 check 阶段执行脚本 。
  • setTimeout 安排在通过最小设定时间后运行的脚本,在timers阶段执行。

大部分时间 setImmediate 会比 setTimeout 先执行,但也有例外。好比下列代码:

setImmediate(() => {
  console.log('setImmediate');
});
setTimeout(() => {
  console.log('setTimeout');
});
复制代码

若是这两个任务是在 check 以后 timer 以前加入到各自阶段的任务队列中的,那么会先执行 setTimeout,其余状况会先执行 setImmediate

总的来讲,setImmediate 在大部分的状况下会比 setTimeout 先执行。

process.nextTick

从技术上来讲,process.nextTick 并不属于 Event Loop 的一部分,它会在每一个阶段执行完毕转入下一个阶段的以前执行。若是有多个 process.nextTick 语句(无论它们是否嵌套),都会在当前阶段结束以后所有执行。

好比:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B() {
    console.log(2);
  });
});

setTimeout(() => {
  console.log('setTimeout');
}, 0);
复制代码

这段代码会输出:“1 => 2 => setTimeout”

再来看下 setImmediate

setImmediate(function A() {
  console.log(1);
  setImmediate(function B() {
    console.log(2);
  });
});

setTimeout(() => {
  console.log('setTimeout');
}, 0);
复制代码

这段代码的老是在最后输出 2,说明 setImmediate 会将它里面的事件注册到下一个循环中。

因为 process.nextTick 里面的 process.nextTick 也会在当前阶段执行,那么若是 process.nextTick 发生了嵌套,那么就会产生无限循环,不再会转入其余阶段。

process.nextTick(function foo() {
  process.nextTick(foo);
});
复制代码

promise

node 中的 promiseprocess.nextTick 都属于微任务,它也会在每一个阶段执行完毕以后调用,可是它的优先级会比 process.nextTick 低。

参考

JavaScript 运行机制详解:再谈Event Loop

The Node.js Event Loop, Timers, and process.nextTick()

相关文章
相关标签/搜索