理解event loop(浏览器环境与nodejs环境)

转自IMWeb社区,做者:sugerpocket,原文连接javascript

众所周知,javascript 是单线程的,其经过使用异步而不阻塞主进程执行。那么,他是如何实现的呢?本文就浏览器与nodejs环境下异步实现与event loop进行相关解释。java

浏览器环境

浏览器环境下,会维护一个任务队列,当异步任务到达的时候加入队列,等待事件循环到合适的时机执行。node

实际上,js 引擎并不仅维护一个任务队列,总共有两种任务git

  1. Task(macroTask): setTimeout, setInterval, setImmediate,I/O, UI rendering
  2. microTask: Promise, process.nextTick, Object.observe, MutationObserver, MutaionObserver

那么两种任务的行为有何不一样呢?github

实验一下,请看下段代码web

setTimeout(function() {
  console.log(4);
}, 0);

var promise = new Promise(function executor(resolve) {
  console.log(1);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve();
  }
  console.log(2);
}).then(function() {
  console.log(5);
});

console.log(3);
复制代码

输出:shell

1 2 3 5 4
复制代码

这说明 Promise.then 注册的任务先执行了。编程

咱们再来看一下以前说的 Promise 注册的任务属于microTask,setTimeout 属于 Task,二者有何差异?windows

实际上,microTasksTasks 并不在同一个队列里面,他们的调度机制也不相同。比较具体的是这样:promise

  1. event-loop start
  2. microTasks 队列开始清空(执行)
  3. 检查 Tasks 是否清空,有则跳到 4,无则跳到 6
  4. 从 Tasks 队列抽取一个任务,执行
  5. 检查 microTasks 是否清空,如有则跳到 2,无则跳到 3
  6. 结束 event-loop

也就是说,microTasks 队列在一次事件循环里面不止检查一次,咱们作个实验

// 添加三个 Task
// Task 1
setTimeout(function() {
  console.log(4);
}, 0);

// Task 2
setTimeout(function() {
  console.log(6);
  // 添加 microTask
  promise.then(function() {
    console.log(8);
  });
}, 0);

// Task 3
setTimeout(function() {
  console.log(7);
}, 0);

var promise = new Promise(function executor(resolve) {
  console.log(1);
  for (var i = 0; i < 10000; i++) {
    i == 9999 && resolve();
  }
  console.log(2);
}).then(function() {
  console.log(5);
});

console.log(3);
复制代码

输出为

1 2 3 5 4 6 8 7
复制代码

microTasks 会在每一个 Task 执行完毕以后检查清空,而此次 event-loop 的新 task 会在下次 event-loop 检测。

Node 环境

实际上,node.js环境下,异步的实现根据操做系统的不一样而有所差别。而不一样的异步方式处理确定也是不相同的,其并无严格按照js单线程的原则,运行环境有可能会经过其余线程完成异步,固然,js引擎仍是单线程的。

node.js使用了Google的V8解析引擎和Marc Lehmann的libev。Node.js将事件驱动的I/O模型与适合该模型的编程语言(Javascript)融合在了一块儿。随着node.js的日益流行,node.js须要同时支持windows, 可是libev只能在Unix环境下运行。Windows 平台上与kqueue(FreeBSD)或者(e)poll(Linux)等内核事件通知相应的机制是IOCP。libuv提供了一个跨平台的抽象,由平台决定使用libev或IOCP。

关于event loop,node.js 环境下与浏览器环境有着巨大差别。

先来一张图

先解释一下各个阶段

  1. timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
  2. I/O callbacks: 执行几乎全部的回调,除了close回调,timer的回调,和setImmediate()的回调。
  3. idle, prepare: 仅内部使用。
  4. poll: 获取新的I/O事件;node会在适当条件下阻塞在这里。
  5. check: 执行setImmediate()设定的回调。
  6. close callbacks: 执行好比socket.on('close', ...)的回调。

每一个阶段的详情

timer

一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间事后,timers会尽量早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。

注意:技术上来讲,poll 阶段控制 timers 何时执行。

I/O callbacks 这个阶段执行一些系统操做的回调。好比TCP错误,如一个TCP socket在想要链接时收到ECONNREFUSED, 类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行。

poll

poll 阶段的功能有两个

  • 执行 timer 阶段到达时间上限的任务。
  • 执行 poll 阶段的任务队列。

若是进入 poll 阶段,而且没有 timer 阶段加入的任务,将会发生如下状况

  • 若是 poll 队列不为空的话,会执行 poll 队列直到清空或者系统回调数达到上限
  • 若是 poll 队列为空
    若是设定了 setImmediate 回调,会直接跳到 check 阶段。 若是没有设定 setImmediate 回调,会阻塞住进程,并等待新的 poll 任务加入并当即执行。
check

这个阶段在 poll 结束后当即执行,setImmediate 的回调会在这里执行。

通常来讲,event loop 确定会进入 poll 阶段,当没有 poll 任务时,会等待新的任务出现,但若是设定了 setImmediate,会直接执行进入下个阶段而不是继续等。

close

close 事件在这里触发,不然将经过 process.nextTick 触发。

一个例子
var fs = require('fs');

function someAsyncOperation (callback) {
  // 假设这个任务要消耗 95ms
  fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

  var delay = Date.now() - timeoutScheduled;

  console.log(delay + "ms have passed since I was scheduled");
}, 100);


// someAsyncOperation要消耗 95 ms 才能完成
someAsyncOperation(function () {

  var startCallback = Date.now();

  // 消耗 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});
复制代码

当event loop进入 poll 阶段,它有个空队列(fs.readFile()还没有结束)。因此它会等待剩下的毫秒, 直到最近的timer的下限时间到了。当它等了95ms,fs.readFile()首先结束了,而后它的回调被加到 poll 的队列并执行——这个回调耗时10ms。以后因为没有其它回调在队列里,因此event loop会查看最近达到的timer的 下限时间,而后回到 timers 阶段,执行timer的回调。

因此在示例里,回调被设定 和 回调执行间的间隔是105ms。

setImmediate() vs setTimeout()

如今咱们应该知道二者的不一样,他们的执行阶段不一样,setImmediate() 在 check 阶段,而settimeout 在 poll 阶段执行。但,还不够。来看一下例子。

// timeout_vs_immediate.js
setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
复制代码
$ node timeout_vs_immediate.js
timeout
immediate
 $ node timeout_vs_immediate.js
immediate
timeout
复制代码

结果竟然是不肯定的,why?

仍是直接给出解释吧。

  1. 首先进入timer阶段,若是咱们的机器性能通常,那么进入timer阶段时,1毫秒可能已通过去了(setTimeout(fn, 0) 等价于setTimeout(fn, 1)),那么setTimeout的回调会首先执行。
  2. 若是没到一毫秒,那么咱们能够知道,在check阶段,setImmediate的回调会先执行。

那咱们再来一个

// timeout_vs_immediate.js
var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
复制代码

输出始终为

$ node timeout_vs_immediate.js
immediate
timeout
复制代码

这个就很好解释了吧。 fs.readFile 的回调执行是在 poll 阶段。当 fs.readFile 回调执行完毕以后,会直接到 check 阶段,先执行 setImmediate 的回调。

process.nextTick()

nextTick 比较特殊,它有本身的队列,而且,独立于event loop。 它的执行也很是特殊,不管 event loop 处于何种阶段,都会在阶段结束的时候清空 nextTick 队列。

参考

juejin.im/entry/58332… jakearchibald.com/2015/tasks-… flyyang.github.io/2017/03/07/… hao5743.github.io/2017/02/27/… github.com/ccforward/c… github.com/creeperyang… developer.mozilla.org/zh-CN/docs/…

相关文章
相关标签/搜索