不要混淆nodejs和浏览器中的event loop

1. 什么是 Event Loop?

"Event Loop是一个程序结构,用于等待和发送消息和事件。 (a programming construct that waits for and dispatches events or messages in a program.)"
复制代码

举一个你们都熟知的栗子, 这样更能客观的理解。node

你们都知道深夜食堂吧。厨师就一我的(服务端 Server)。最多再来一个服务生( 调度员 Event Loop )。晚上吃饭的客人(客户端 Client)不少。数据库

1. 客人向服务生点完菜,就干本身事情,不用一直等着服务生, 服务生把一我的点的菜单送到厨师,
   又去服务新的客人...
2. 厨师(服务端)只负责作客人们点的菜。
3. 服务生(调度员)不停的看厨师,一旦厨师作好菜了,按照标号送到相应的 客人(客户端)座位上
复制代码

假设咱们把 厨师 和 服务生 都比做 服务端的线程的话, 服务生线程 为 主线程, 厨师线程 为 消息线程。 客人每点一个菜。服务生就向厨师发出一个消息。并保留该消息的“标识”( 回调函数)用来接收厨师炒好的菜,并把菜送到相应的客人手中。

2. 同步模式

无论店里的客人多少,也无论每一份菜须要多久的时间作好。就只有厨师这一我的忙活。厨师一次只能服务一个客人。那这样的服务模式效率就比较低了。中途等待的时间比较长。 笔者认为 同步模式 就是没有 “服务生线程”, 厨师线程升级为 主线程api

1. 第一个客人点了一份 "读取文件" ,  炒好一份 "读取文件"  须要花费 1 分钟
2. 必须等第一个客人的菜炒好后,第二个客人才能点,而且点了一份 "读取数据库",
   炒好一份 "读取数据库" 须要花费 2 分钟
3. 第三个客人点了一份 ...
复制代码

从图中能够看出红色部份都是等待时间(或者是阻塞时间), 至关浪费资源。promise

假设咱们如今只知道一种代码的执行方式 "同步执行", 也就是代码从上到下 按顺序执行。若是遇到 setTimeout , 也先这样理解。(实际上setTimeout 自己是当即执行的,只是回调函数异步执行)浏览器

console.log(1);                         //执行顺序1
setTimeout(function(){}, 1000);         //执行顺序2
console.log(2);                         //执行顺序3
复制代码

3. 异步模式

图表更能直观的反应这个概念: bash

主线程 不停的接收请求 request 和 响应请求 response, 真正处理任务的被 消息线程 event loop 安排其余相应的程序去执行,并接收相应的相应程序返回的消息。而后 reponse 给客户端。多线程

1. 主线程干的事情很是简单,即 接收请求,响应请求, 所以能够可以处理更多的请求。而不用等待。
2. 消息线程维护请求,并把真正要作的事情交给对应的程序,并接收对应程序的回调消息,返回给 主线程
复制代码

4. 几种调用模式的组合

  • 同步阻塞

你跟你的女神表白,你女神当即回复你,而你也一直再等女神的回复异步

  • 同步不阻塞

你跟你的女神表白, 你表白后,没有等女神来得及回复,你去忙你本身的事情了。你的女神当即回复了你socket

  • 异步阻塞

你跟你的女神表白, 你女神没有当即回复你,说要考虑考虑,过几天答复你,而你也一直再等女神的回复函数

  • 异步不阻塞

你跟你的女神表白,你表白后, 没有等女神的回复。你去忙你本身的事情了,女神也说她要考虑考虑,过几天再回复你

阻塞非阻塞 是指调用者(表白的那我的) 同步异步 是指被调用者 (被表白的那我的)

同步异步取决于被调用者,阻塞非阻塞取决于调用者

5. 几个须要知晓的概念

  • 宏任务 setTimeout , setInterval, setImmediate, I / O 操做

  • 微任务 process.nextTick , 原生Promise (有些实现的Promise将then方法放到了宏任务中), Mutation Observer

console.log(1);
Promise.resolve('123').then(()=>{console.log('then')})
process.nextTick(function () {
  console.log('nextTick')
})
console.log(2);
复制代码

process.nextTick 优先于 promise.then 方法执行

6. 浏览器中的Event Loop

  • 浏览器中js是单线程执行的。笔者称其为主线程, 主线程在运行过程当中会产生 堆(heap)和 栈(stack), 全部同步任务都是在 栈中执行。
function one() {
  let a = 1;
  two();
  function two() {
    console.log(a);
    let b = 2;
    function three() {
      //debugger;
      console.log(b);
    }
    three();
  }
}
one();
复制代码

毫无疑问的是,上面这段代码执行的结果为:

1
2
复制代码

在栈中都是以同步任务的方式存在:

再来看下面这段代码:

console.log(1);
setTimeout(function(){
  console.log(2);
})
console.log(3);
复制代码

执行结果为:

1
3
2
复制代码

那究竟是怎样执行的呢?

顺便提一句:文章最开始就说 setTimeout函数自己的执行时机和其回调函数执行的时机是不同的。

//宏任务
setTimeout(function(){
  console.log(2);
})

//微任务
let p = new Promise((resolve, reject) => {
  resolve(3);
});
p.then((data) => {
  console.log(data);
}, (err)=>{

})

console.log(4);
复制代码

执行结果为:

1
4
3
2
复制代码

从这个能够看到。微任务消息队列的执行的优先于宏任务的消息队列.

console.log(1);

//宏任务
setTimeout(function(){
  console.log(2);
})

//微任务
let p = new Promise((resolve, reject) => {
  resolve(4);
});
p.then((data) => {
  console.log(data);
}, (err)=>{

})

setTimeout(function(){
  console.log(3);
})


console.log(5);

复制代码

执行结果为:

1
5
4
2
3
复制代码

每一次事件循环机制过程当中,会将当前宏任务 或者 微任务消息队列中的任务都执行完成。而后再以前其余队列。

  • 对于不能进入主线程执行的代码,笔者称其为异步任务, 这部分任务会进去消息队列(callback queue), 经过 事件循环机制 (event loop) 不停调用,进入 栈中进行执行。前提是栈中当前的全部任务(同步任务)都已经执行完成。

  • 从图中,还能够得出这样的结论: 异步任务是经过 WebAPIs 的方式存入 消息队列。
  • 上述过程老是在循环执行。

7. Node中的Event Loop

咱们先来看看node是怎样运行的:

  • js源码首先交给node 中的v8引擎进行编译
  • 编译好的js代码经过node api 交给 libuv库 处理
  • libuv库经过阻塞I/O和异步的方式,为每个js任务(文件读取等等)建立一个单独的线程,造成多线程
  • 经过Event Loop的方式异步的返回每个任务执行的结果,而后返回给V8引擎,并反馈给用户

Event Loop 在整个Node 运行机制中占据着举足轻重的地位。是其核心。

(Event Loop 不一样阶段)

每一个阶段都有一个执行回调的FIFO队列。 虽然每一个阶段都有其特定的方式,但一般状况下,当事件循环进入给定阶段时,它将执行特定于该阶段的任何操做, 而后在该阶段的队列中执行回调,直到队列耗尽或回调的最大数量 已执行。 当队列耗尽或达到回调限制时,事件循环将移至下一个阶段,依此类推。

timers:此阶段执行由setTimeout()和setInterval()调度的回调。
pending callbacks:执行I / O回调,推迟到下一个循环迭代。
idle,prepare:只在内部使用。
poll:检索新的I / O事件; 执行I / O相关的回调函数;  适当时节点将在此处阻塞。
check:setImmediate()回调在这里被调用。
close backbacks:一些关闭回调,例如 socket.on('close',...)。
复制代码

timers阶段

须要注意的是:

const fs = require('fs');

function someAsyncOperation(callback) {
  //假设须要95ms须要执行完成
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

//定义100ms后执行
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);


// 执行someAsyncOperation须要消耗95ms执行
someAsyncOperation(() => {
  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }
});
复制代码

分析上述代码:

  • someAsyncOperation方法时同步代码,先在栈中执行
  • someAsyncOperation 中包含异步I/O, 须要花费95ms执行,加上 while的10ms, 所以须要105ms
  • setTimeout 虽然定义的是在100ms后执行, 但因为 第一次轮询是到了 poll 阶段, 因此 setTimeout 须要等到第二轮事件轮询是执行。所以是在 105ms后执行

pending callbacks阶段

此阶段为某些系统操做(如TCP错误类型)执行回调。例如,
若是尝试链接时TCP套接字收到ECONNREFUSED,则某些* nix系统要等待报告错误。这将排队等候在待处理的回调阶段执行。
复制代码

poll阶段

1.计算应该阻塞和轮询I / O的时间
2.处理轮询队列中的事件。
复制代码

当事件循环进入poll阶段而且没有计时器时,会发生如下两件事之一:

1. 若是轮询队列不为空,则事件循环将遍历其回调队列,同步执行它们,直到队列耗尽或达到系统相关硬限制。
2. 若是轮询队列为空,则会发生如下两件事之一:
   2.1 若是脚本已经过setImmediate()进行调度,则事件循环将结束轮询阶段并继续执行(check阶段)检查阶段以执行这些预约脚本。
   2.2 若是脚本没有经过setImmediate()进行调度,则事件循环将等待将回调添加到队列中,而后当即执行它们。
复制代码

一旦轮询队列为空,事件循环将检查已达到时间阈值的定时器。若是一个或多个定时器准备就绪,则事件循环将回退到定时器阶段以执行这些定时器的回调。

// poll的下一个阶段时check
// 有check阶段就会走到check中
let fs = require('fs');
fs.readFile('./1.txt',function () {  //轮询队列已经执行完成,为空,即2.1中描述的
  setTimeout(() => {
    console.log('setTimeout')
  }, 0);
  setImmediate(() => {
    console.log('setImmediate')
  });
});
复制代码

上面这段代码执行的过程阶段为:

check阶段

setImmediate()其实是一个特殊的定时器,它在事件循环的一个单独的阶段中运行。它使用libuv API来调度回调,以在轮询(poll)阶段完成后执行。

close callback阶段

若是套接字socks或句柄忽然关闭(例如socket.destroy()),则在此阶段将发出'close'事件。 不然它将经过process.nextTick()触发事件。
复制代码

8. setImmediate 与 setTimeout

setImmediate()用于在当前轮询阶段完成后执行脚本。
setTimeout()计划脚本在通过最小阈值(以毫秒为单位)后运行。
复制代码

定时器执行的顺序取决于它们被调用的上下文。 若是二者都是在主模块内调用的,那么时序将受到进程性能的限制(可能会受到计算机上运行的其余应用程序的影响)。

简言之: setTimediate 和 setTimeout 的执行顺序不肯定。

// setTimeout和setImmediate顺序是不固定,看node准备时间
 setTimeout(function () {
   console.log('setTimeout')
 },0);

 setImmediate(function () {
   console.log('setImmediate')
 });

复制代码

输出的结果多是这样

setTimeout
setImmediate
复制代码

也有多是这样

setImmediate
setTimeout
复制代码

But, 若是在I / O周期内移动这两个调用,则当即回调老是首先执行, 能够爬楼参考 poll阶段的介绍。

使用setImmediate()的主要优势是,若是在I / O周期内进行调度,将始终在任何计时器以前执行setImmediate(),而无论有多少个计时器。

9. process.nextTick

为何要用process.nextTick

容许用户处理错误,清理任何不须要的资源,或者可能在事件循环继续以前再次尝试请求。 有时须要在调用堆栈解除以后但事件循环继续以前容许回调运行。

process.nextTick()没有显示在图中,即便它是异步API的一部分。 这是由于process.nextTick()在技术上并非事件循环的一部分。 相反,nextTickQueue将在当前操做完成后处理,而无论事件循环的当前阶段如何。

回顾一下事件循环机制,只要你在给定的阶段调用process.nextTick(),全部传递给process.nextTick()的回调都将在事件循环继续以前被解析。

// nextTick是队列切换时执行的,timer->check队列 timer1->timer2不叫且
setImmediate(() => {
  console.log('setImmediate1')
  setTimeout(() => {
    console.log('setTimeout1')
  }, 0);
})
setTimeout(()=>{
  process.nextTick(()=>console.log('nextTick'))
  console.log('setTimeout2')
  setImmediate(()=>{
    console.log('setImmediate2')
  })
},0);
复制代码

在讨论事件循环(Event Loop)的时候,要时刻知道 宏任务,微任务,process.nextTick等概念。 上面代码执行的结果可能为:

setTimeout2
nextTick
setImmediate1
setImmediate2
setTimeout1
复制代码

或者

setImmediate1
setTimeout2
setTimeout1
nextTick
setImmediate2
复制代码

为何呢? 这个就留给各位看官的一个思考题吧。欢迎留言讨论~

相关文章
相关标签/搜索