[关闭]
@Dreamingboy 2017-12-10T16:40:26.000000Z 字数 6799 阅读 1180

浏览器端的EventLoop和node的EventLoop的区别

node


最近在学习node,看了一些关于node的eventLoop的文章之后得出自己的一些结论,跟大家分享一下,如果有错误的地方,也希望大家指出,多多指教!

说到JavaScript的异步,绕不开的就是EventLoop,这是JavaScript实现异步的基础,而浏览器端的EventLoop和node的又有很大的不同,下面,就让我们来探讨一下他们之间的区别。

1浏览器EventLoop

1.1浏览器端异步

浏览器端的异步主要包含下面几个方面:

1.2浏览器端的EventLoop的基本模型

image.png-65.6kB

上面的很好的解释了浏览器端的异步是如何实现的,下面先看一个代码片段来理解一下:

  1. (function() {
  2. console.log('this is the start');
  3. setTimeout(function cb() {
  4. console.log('this is a msg from call back');
  5. });
  6. console.log('this is just a message');
  7. setTimeout(function cb1() {
  8. console.log('this is a msg from call back1');
  9. }, 0);
  10. console.log('this is the end');
  11. })();
  12. //输出结果
  13. // "this is the start"
  14. // "this is just a message"
  15. // "this is the end"
  16. // "this is a msg from call back"
  17. // "this is a msg from call back1"

我们都知道JavaScript是单线程的,但是这个单线程的意思是执行代码是单线程的,这个单线程就是上图的stack,所有的同步代码都是在这里执行的。而异步操纵只会在同步操作执行完之后才会开始执行。上面这段代码的执行经历下面这些过程:

  1. 代码从上往下执行,先打印出“this is a msg from call back ”,
  2. 解析到第一个setTimeout,而且这个setTimeout没有给出具体的时间参数,那么此时就会默认时间参数是0(需要注意的是0不代表立即将setTimeout的回调函数加入到事件队列中,而是由一定的最小时间限制的),在经过最小时间限制后,就会将setTimeout的回调函数加入到事件队列中。
  3. 另外一个同步操作,打印出“this is just a message”
  4. 接下来是另外一个setTimeout的回调函数被加入到事件队列中,最后的同步操作打印出“this is the end”。
  5. stack按照加入callbackQueue的回调函数的顺序从callbackQueue中拿出回调函数执行,首先是打印"this is a msg from call back",然后是打印"this is a msg from call back1"

这就是浏览器端的EventLoop的整个执行过程,这个过程相对来说还是比较简单易懂的。

2 node的EventLoop

2.1 基本模型

image.png-75.8kB

我们可以看到,node的EventLoop的模型要比浏览器端的复杂很多,下面让我们来一步步进行讲解。

2.1.1 phase

上图中的每一个矩形代表的是事件队列的每一个阶段(官网上叫做“phase”),他们具体负责的工作为(附上官方文档的解释):

每个phase代表不同的时期,eventLoop每次轮询都会从timers开始将EventLoop里面的回调函数推导主线程中执行,直到执行完这个phase里面的回调函数或者是执行的数量达到允许的最大值(在每个循环周期中允许执行的函数数目是有限的)后才会进入到下一个phase,按照这样的顺序直到这个周期结束后再进入下个周期。当然,这里面还有很多细节,待会会详细讨论。

这里就可以看出node的EventLoop和浏览器端的EventLoop的很大的不同:浏览器端没有阶段的区分,只会按照回调函数进入事件队列的顺序进行执行,node则会按照不同类型的回调函数在不同阶段有区别地执行

2.1.2 timers

setTimeout()和setInterval实际上和浏览器端的作用原理是一样的,在指定的时间后将绑定的回调函数添加到事件队列中,而且,当事件设置为0时,也不是立即就将回调函数添加到事件队列的timers中,下面这段是引用官网的说明:

When delay is larger than 2147483647 or less than 1, the delay will be set to 1.

下面看看官网的这个例子:

  1. const fs = require('fs');
  2. function someAsyncOperation(callback) {
  3. // Assume this takes 95ms to complete
  4. fs.readFile('/path/to/file', callback);
  5. }
  6. const timeoutScheduled = Date.now();
  7. setTimeout(() => {
  8. const delay = Date.now() - timeoutScheduled;
  9. console.log(`${delay}ms have passed since I was scheduled`);
  10. }, 100);
  11. // do someAsyncOperation which takes 95 ms to complete
  12. someAsyncOperation(() => {
  13. const startCallback = Date.now();
  14. // do something that will take 10ms...
  15. while (Date.now() - startCallback < 10) {
  16. // do nothing
  17. }
  18. });

让我们来分析一下上面这段代码的是如何执行的:

  1. 代码从上向下解析,遇到第一个异步操作-setTimeout,于是将在100ms后将setTimeout绑定的函数添加到timers中
  2. 继续向下解析,执行函数someAsyncOperation,函数someAsyncOperation首先读取文件,这个过程花费了95ms
  3. 由于在执行执行同步代码的时候没有回调函数添加到事件队列中,所以在进入poll阶段时事件队列还是空的,此时poll会等待到有新的事件被触发。
  4. 在文件读取结束后将其绑定的回调函数添加到事件队列中,poll将回调函数推到主线程里面执行,由于这个函数要执行10ms,在执行到5ms时setTimeout绑定的回调函数会被添加到timers中去
  5. 等到fs.read()绑定的函数执行完毕后poll检测到timers中有绑定的函数,而且check和close callback如果有绑定函数,就会进入下个阶段,由于下两个阶段(check和close callback)都没有绑定函数,那么就会重新回到timers,此时timers中有函数就推进主线程中执行,所以,最后输出的结果是延迟了105ms

但是,现在有一个问题,如果fs.read()要执行很长时间,那岂不是导致阻塞了吗?答案是:否;

先看一下官方文章里面的几个句子:

Note: Technically, the poll phase controls when timers are executed.

When the event loop enters the poll phase, it has an empty queue (fs.readFile() has not completed), so it will wait for the number of ms remaining until the soonest timer's threshold is reached.

Once the poll queue is empty the event loop will check for timers whose time thresholds have been reached. If one or more timers are ready, the event loop will wrap back to the timers phase to execute those timers' callbacks.

上面第一个句子说明了timers其实是受到poll影响的,后面两句说明了如果fs.read()执行的时间太长,超过了timers设定的时间,EventLoop会将timers绑定的函数推到主线程中执行(下面讲解poll的时候会解释),这样避免了阻塞问题

2.1.3 poll

这个阶段的功能是最复杂的。先看一下官方文档的解释:

The poll phase has two main functions:

  • Executing scripts for timers whose threshold has elapsed,
  • then Processing events in the poll queue.

When the event loop enters the poll phase and there are no timers scheduled, one of two things will happen:

  • If the poll queue is not empty, the event loop will iterate through its queue of callbacks executing them synchronously until either the queue has been exhausted, or the system-dependent hard limit is reached.
  • If the poll queue is empty, one of two more things will happen:

    • If scripts have been scheduled by setImmediate(), the event loop will end the poll phase and continue to the check phase to execute those scheduled scripts.

    • If scripts have not been scheduled by setImmediate(), the event loop will wait for callbacks to be added to the queue, then execute them immediately.

Once the poll queue is empty the event loop will check for timers whose time thresholds have been reached. If one or more timers are ready, the event loop will wrap back to the timers phase to execute those timers' callbacks.

首先,上面的这段话说明了poll具有两个功能:

也就是说通过setTimeout绑定的回调函数有可能会在两个阶段被推到主线程中执行:timers和poll(下面会给出例子解释),但是timers绑定的函数能够在poll阶段被执行的条件是:计时器计时结束(下面会有例子解释)

poll的这个阶段回调函数执行还分多种情况:

还是上面那个例子:

  1. const fs = require('fs');
  2. function someAsyncOperation(callback) {
  3. // Assume this takes 95ms to complete
  4. fs.readFile('/path/to/file', callback);
  5. }
  6. const timeoutScheduled = Date.now();
  7. setTimeout(() => {
  8. const delay = Date.now() - timeoutScheduled;
  9. console.log(`${delay}ms have passed since I was scheduled`);
  10. }, 100);
  11. // do someAsyncOperation which takes 95 ms to complete
  12. someAsyncOperation(() => {
  13. const startCallback = Date.now();
  14. // do something that will take 10ms...
  15. while (Date.now() - startCallback < 10) {
  16. // do nothing
  17. }
  18. });

具体的解释可以看上面

2.2 setTimeout和setImmediate

两者在很多方面和相似,但是他们的区别还是很大的,而且setTimeout具有的一个缺点是时间的不确定性。先来看看官方给的例子:

  1. // timeout_vs_immediate.js
  2. setTimeout(() => {
  3. console.log('timeout');
  4. }, 0);
  5. setImmediate(() => {
  6. console.log('immediate');
  7. });
  8. //结果
  9. $ node timeout_vs_immediate.js
  10. immediate
  11. timeout
  12. $ node timeout_vs_immediate.js
  13. immediate
  14. timeout

可以看到可能有两种结果,为什么呢?因为虽然setTimeout的时间设置为0,但是实际上系统会将其设置为1,所以如果上面的同步代码解析到setImmediate时时间小于1ms,那么就会先将setImmediate加入到事件队列中,反之,则会是将setTimeout加入到事件队列中。

再来看看下面这个例子:

  1. // timeout_vs_immediate.js
  2. const fs = require('fs');
  3. fs.readFile(__filename, () => {
  4. setTimeout(() => {
  5. console.log('timeout');
  6. }, 0);
  7. setImmediate(() => {
  8. console.log('immediate');
  9. });
  10. });
  11. //
  12. $ node timeout_vs_immediate.js
  13. immediate
  14. timeout
  15. $ node timeout_vs_immediate.js
  16. immediate
  17. timeout

上面这个例子只有一种结果,这是为什么呢?因为首先会进行文件的读取,然后就会将fs.readFile的回调函数加入到事件队列中,文件读取结束后执行回调函数,此时EventLoop还没有到达check这个阶段,文件读取绑定回调函数中分别在timers和check阶段绑定了回调函数,所以EventLoop经过poll阶段后进入check阶段执行setImmediate绑定的函数,然后再绕回timers执行setTimeout绑定的函数,这样就得到上面的结果。

2.3 process.nextTick()和setImmediate()

这两个方法的实际用途和他们的名字相反,前者是当前的同步执行结束后直接执行其绑定的函数,而setImmediate是只在check阶段才执行。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注