一篇文章教会你Event loop——浏览器和Node

最近对Event loop比较感兴趣,因此了解了一下。可是发现整个Event loop尽管有不少篇文章,可是没有一篇能够看完就对它全部内容都了解的文章。大部分的文章都只阐述了浏览器或者Node两者之一,没有对比的去看的话,认识老是浅一点。因此才有了这篇整理了百家之长的文章。

1. 定义

Event loop:为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。(3月29修订)

那什么是事件?html

事件:事件就是因为某种外在或内在的信息状态发生的变化,从而致使出现了对应的反应。好比说用户点击了一个按钮,就是一个事件;HTML页面完成加载,也是一个事件。一个事件中会包含多个任务。

咱们在以前的文章中提到过,JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不一样,Event loop也有不一样的实现:其中Node使用了libuv库来实现Event loop; 而在浏览器中,html规范定义了Event loop,具体的实现则交给不一样的厂商去完成。node

因此,浏览器的Event loop和Node的Event loop是两个概念,下面分别来看一下。git

2. 意义

在实际工做中,了解Event loop的意义能帮助你分析一些异步次序的问题(固然,随着es7 async和await的流行,这样的机会愈来愈少了)。除此之外,它还对你了解浏览器和Node的内部机制有积极的做用;对于参加面试,被问到一堆异步操做的执行顺序时,也不至于两眼抓瞎。github

3. 浏览器上的实现

在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。它们分别包含如下内容:面试

MacroTask: script(总体代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering
MicroTask: process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

须要注意的一点是:在同一个上下文中,总的执行顺序为同步代码—>microTask—>macroTask[6]。这一块咱们在下文中会讲。segmentfault

浏览器中,一个事件循环里有不少个来自不一样任务源的任务队列(task queues),每个任务队列里的任务是严格按照先进先出的顺序执行的。可是,由于浏览器本身调度的关系,不一样任务队列的任务的执行顺序是不肯定的。promise

具体来讲,浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),若是不为空则会一次性执行完全部microtask。而后再进入下一个循环去task队列中取下一个task执行,以此类推。浏览器

图片描述

注意:图中橙色的MacroTask任务队列也应该是在不断被切换着的。网络

本段大批量引用了《什么是浏览器的事件循环(Event Loop)》的相关内容,想看更加详细的描述能够自行取用。异步

4. Node上的实现

nodejs的event loop分为6个阶段,它们会按照顺序反复运行,分别以下:

  1. timers:执行setTimeout() 和 setInterval()中到期的callback。
  2. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
  3. idle, prepare:队列的移动,仅内部使用
  4. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
  5. check:执行setImmediate的callback
  6. close callbacks:执行close事件的callback,例如socket.on("close",func)

不一样于浏览器的是,在每一个阶段完成后,而不是MacroTask任务完成后,microTask队列就会被执行。这就致使了一样的代码在不一样的上下文环境下会出现不一样的结果。咱们在下文中会探讨。

另外须要注意的是,若是在timers阶段执行时建立了setImmediate则会在此轮循环的check阶段执行,若是在timers阶段建立了setTimeout,因为timers已取出完毕,则会进入下轮循环,check阶段建立timers任务同理。

图片描述

5. 示例

5.1 浏览器与Node执行顺序的区别

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)



浏览器输出:
time1
promise1
time2
promise2

Node输出:
time1
time2
promise1
promise2

在这个例子中,Node的逻辑以下:

最初timer1和timer2就在timers阶段中。开始时首先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,一样的步骤执行timer2,打印timer2;
至此,timer阶段执行结束,event loop进入下一个阶段以前,执行microtask队列的全部任务,依次打印promise一、promise2。

而浏览器则由于两个setTimeout做为两个MacroTask, 因此先输出timer1, promise1,再输出timer2,promise2。

更加详细的信息能够查阅《深刻理解js事件循环机制(Node.js篇)

为了证实咱们的理论,把代码改为下面的样子:

setImmediate(() => {
  console.log('timer1')

  Promise.resolve().then(function () {
    console.log('promise1')
  })
})

setTimeout(() => {
  console.log('timer2')

  Promise.resolve().then(function () {
    console.log('promise2')
  })
}, 0)

Node输出:
timer1               timer2
promise1    或者     promise2
timer2               timer1
promise2             promise1

按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果,为何会出现两种结果呢?
这是由于Node 作不到0毫秒,最少也须要1毫秒。实际执行的时候,进入事件循环之后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的情况。若是没到1毫秒,那么 timers 阶段就会跳过,进入 check 阶段,先执行setImmediate的回调函数。

另外,若是已通过了Timer阶段,那么setImmediate会比setTimeout更快,例如:

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上面代码会先进入 I/O callbacks 阶段,而后是 check 阶段,最后才是 timers 阶段。所以,setImmediate才会早于setTimeout执行。

具体能够看《Node 定时器详解》。

5.2 不一样异步任务执行的快慢

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

Promise.resolve().then(() => console.log(3));
process.nextTick(() => console.log(4));


输出结果:4 3 1 2或者4 3 2 1

由于咱们上文说过microTask会优于macroTask运行,因此先输出下面两个,而在Node中process.nextTick比Promise更加优先[3],因此4在3前。而根据咱们以前所说的Node没有绝对意义上的0ms,因此1,2的顺序不固定。

5.3 MicroTask队列与MacroTask队列

setTimeout(function () {
       console.log(1);
   },0);
   console.log(2);
   process.nextTick(() => {
       console.log(3);
   });
   new Promise(function (resolve, rejected) {
       console.log(4);
       resolve()
   }).then(res=>{
       console.log(5);
   })
   setImmediate(function () {
       console.log(6)
   })
   console.log('end');

Node输出:
2 4 end 3 5 1 6

这个例子来源于《JavaScript中的执行机制》。Promise的代码是同步代码,then和catch才是异步的,因此4要同步输出,而后Promise的then位于microTask中,优于其余位于macroTask队列中的任务,因此5会优于1,6输出,而Timer优于Check阶段,因此1,6。

6. 总结

综上,关于最关键的顺序,咱们要依据如下几条规则:

  1. 同一个上下文下,MicroTask会比MacroTask先运行
  2. 而后浏览器按照一个MacroTask任务,全部MicroTask的顺序运行,Node按照六个阶段的顺序运行,并在每一个阶段后面都会运行MicroTask队列
  3. 同个MicroTask队列下process.tick()会优于Promise

Event loop仍是比较深奥的,深刻进去会有不少有意思的东西,有任何问题还望不吝指出。

参考文档:

  1. 什么是浏览器的事件循环(Event Loop)
  2. 不要混淆nodejs和浏览器中的event loop
  3. Node 定时器详解
  4. 浏览器和Node不一样的事件循环(Event Loop)
  5. 深刻理解js事件循环机制(Node.js篇)
  6. JavaScript中的执行机制
相关文章
相关标签/搜索