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

1. 什么是 Event Loop?

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

举一个大家都熟知的栗子, 这样更能客观的理解。

大家都知道深夜食堂吧。厨师就一个人(服务端 Server)。最多再来一个服务生( 调度员 Event Loop )。晚上吃饭的客人(客户端 Client)很多。

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

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

2. 同步模式

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

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

从图中可以看出红色部份都是等待时间(或者是阻塞时间), 相当浪费资源。

假设我们现在只知道一种代码的执行方式 "同步执行", 也就是代码从上到下 按顺序执行。如果遇到 setTimeout , 也先这样理解。(实际上setTimeout 本身是立即执行的,只是回调函数异步执行)

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

3. 异步模式

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

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

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

4. 几种调用模式的组合

  • 同步阻塞

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

  • 同步不阻塞

你跟你的女神表白, 你表白后,没有等女神来得及回复,你去忙你自己的事情了。你的女神立即回复了你

  • 异步阻塞

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

  • 异步不阻塞

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

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

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

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
复制代码

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

转载于:https://juejin.im/post/5b0524f8518825428a2631ee

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值