日志

nodejs事件和事件循环详解

 来源    2021-01-14    1  

简介

上篇文章我们简单的介绍了nodejs中的事件event和事件循环event loop。本文本文将会更进一步,继续讲解nodejs中的event,并探讨一下setTimeout,setImmediate和process.nextTick的区别。

nodejs中的事件循环

虽然nodejs是单线程的,但是nodejs可以将操作委托给系统内核,系统内核在后台处理这些任务,当任务完成之后,通知nodejs,从而触发nodejs中的callback方法。

这些callback会被加入轮循队列中,最终被执行。

通过这样的event loop设计,nodejs最终可以实现非阻塞的IO。

nodejs中的event loop被分成了一个个的phase,下图列出了各个phase的执行顺序:

每个phase都会维护一个callback queue,这是一个FIFO的队列。

当进入一个phase之后,首先会去执行该phase的任务,然后去执行属于该phase的callback任务。

当这个callback队列中的任务全部都被执行完毕或达到了最大的callback执行次数之后,就会进入下一个phase。

注意, windows和linux的具体实现有稍许不同,这里我们只关注最重要的几个phase。

问题:phase的执行过程中,为什么要限制最大的callback执行次数呢?

回答:在极端情况下,某个phase可能会需要执行大量的callback,如果执行这些callback花费了太多的时间,那么将会阻塞nodejs的运行,所以我们设置callback执行的次数限制,以避免nodejs的长时间block。

phase详解

上面的图中,我们列出了6个phase,接下来我们将会一一的进行解释。

timers

timers的中文意思是定时器,也就是说在给定的时间或者时间间隔去执行某个callback函数。

通常的timers函数有这样两种:setTimeout和setInterval。

一般来说这些callback函数会在到期之后尽可能的执行,但是会受到其他callback执行的影响。 我们来看一个例子:

const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;

  console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
  const startCallback = Date.now();

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

上面的例子中,我们调用了someAsyncOperation,这个函数首先回去执行readFile方法,假设这个方法耗时95ms。接着执行readFile的callback函数,这个callback会执行10ms。最后才回去执行setTimeout中的callback。

所以上面的例子中,虽然setTimeout指定要在100ms之后运行,但是实际上还要等待95 + 10 = 105 ms之后才会真正的执行。

pending callbacks

这个phase将会执行一些系统的callback操作,比如在做TCP连接的时候,TCP socket接收到了ECONNREFUSED信号,在某些liunx操作系统中将会上报这个错误,那么这个系统的callback将会放到pending callbacks中运行。

或者是需要在下一个event loop中执行的I/O callback操作。

idle, prepare

idle, prepare是内部使用的phase,这里就不过多介绍。

poll轮询

poll将会检测新的I/O事件,并执行与I / O相关的回调,注意这里的回调指的是除了关闭callback,timers,和setImmediate之外的几乎所有的callback事件。

poll主要处理两件事情:轮询I/O,并且计算block的时间,然后处理poll queue中的事件。

如果poll queue非空的话,event loop将会遍历queue中的callback,然后一个一个的同步执行,知道queue消费完毕,或者达到了callback数量的限制。

因为queue中的callback是一个一个同步执行的,所以可能会出现阻塞的情况。

如果poll queue空了,如果代码中调用了setImmediate,那么将会立马跳到下一个check phase,然后执行setImmediate中的callback。 如果没有调用setImmediate,那么会继续等待新来的callback被加入到queue中,并执行。

check

主要来执行setImmediate的callback。

setImmediate可以看做是一个运行在单独phase中的独特的timer,底层使用的libuv API来规划callbacks。

一般来说,如果在poll phase中有callback是以setImmediate的方式调用的话,会在poll queue为空的情况下,立马结束poll phase,进入check phase来执行对应的callback方法。

close callbacks

最后一个phase是处理close事件中的callbacks。 比如一个socket突然被关闭,那么将会触发一个close事件,并调用相关的callback。

setTimeout 和 setImmediate的区别

setTimeout和setImmediate有什么不同呢?

从上图的phase阶段可以看出,setTimeout中的callback是在timer phase中执行的,而setImmediate是在check阶段执行的。

从语义上讲,setTimeout指的是,在给定的时间之后运行某个callback。而setImmediate是在执行完当前loop中的 I/O操作之后,立马执行。

那么这两个方法的执行顺序上有什么区别呢?

下面我们举两个例子,第一个例子中两个方法都是在主模块中运行:

setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

这样运行两个方法的执行顺序是不确定,因为可能受到其他执行程序的影响。

第二个例子是在I/O模块中运行这两个方法:

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

你会发现,在I/O模块中,setImmediate一定会在setTimeout之前执行。

两者的共同点

setTimeout和setImmediate两者都有一个返回值,我们可以通过这个返回值,来对timer进行clear操作:

const timeoutObj = setTimeout(() => {
  console.log('timeout beyond time');
}, 1500);

const immediateObj = setImmediate(() => {
  console.log('immediately executing immediate');
});

const intervalObj = setInterval(() => {
  console.log('interviewing the interval');
}, 500);

clearTimeout(timeoutObj);
clearImmediate(immediateObj);
clearInterval(intervalObj);

clear操作也可以clear intervalObj。

unref 和 ref

setTimeout和setInterval返回的对象都是Timeout对象。

如果这个timeout对象是最后要执行的timeout对象,那么可以使用unref方法来取消其执行,取消执行完毕,可以使用ref来恢复它的执行。

const timerObj = setTimeout(() => {
  console.log('will i run?');
});

timerObj.unref();

setImmediate(() => {
  timerObj.ref();
});

注意,如果有多个timeout对象,只有最后一个timeout对象的unref方法才会生效。

process.nextTick

process.nextTick也是一种异步API,但是它和timer是不同的。

如果我们在一个phase中调用process.nextTick,那么nextTick中的callback会在这个phase完成,进入event loop的下一个phase之前完成。

这样做就会有一个问题,如果我们在process.nextTick中进行递归调用的话,这个phase将会被阻塞,影响event loop的正常执行。

那么,为什么我们还会有process.nextTick呢?

考虑下面的一个例子:

let bar;

function someAsyncApiCall(callback) { callback(); }

someAsyncApiCall(() => {
  console.log('bar', bar); // undefined
});

bar = 1;

上面的例子中,我们定义了一个someAsyncApiCall方法,里面执行了传入的callback函数。

这个callback函数想要输出bar的值,但是bar的值是在someAsyncApiCall方法之后被赋值的。

这个例子最终会导致输出的bar值是undefined。

我们的本意是想让用户程序执行完毕之后,再调用callback,那么我们可以使用process.nextTick来对上面的例子进行改写:

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

我们再看一个实际中使用的例子:

const server = net.createServer(() => {}).listen(8080);

server.on('listening', () => {});

上面的例子是最简单的nodejs创建web服务。

上面的例子有什么问题呢?listen(8000) 方法将会立马绑定8000端口。但是这个时候,server的listening事件绑定代码还没有执行。

这里实际上就用到了process.nextTick技术,从而不管我们在什么地方绑定listening事件,都可以监听到listen事件。

process.nextTick 和 setImmediate 的区别

process.nextTick 是立马在当前phase执行callback,而setImmediate是在check阶段执行callback。

所以process.nextTick要比setImmediate的执行顺序优先。

实际上,process.nextTick和setImmediate的语义应该进行互换。因为process.nextTick表示的才是immediate,而setImmediate表示的是next tick。

本文作者:flydean程序那些事

本文链接:http://www.flydean.com/nodejs-event-more/

本文来源:flydean的博客

欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

selenium+python自动化94-行为事件(ActionChains)源码详解
日志ActionChains简介 actionchains是selenium里面专门处理鼠标相关的操作如:鼠标移动,鼠标按钮操作,按键和上下文菜单(鼠标右键)交互. 这对于做更复杂的动作非常有用,比如悬停 ...
1
javascript事件中'return false'详解
日志浏览器中有很多异步事件,如click,mouseenter,mouseover等等,当用户执行相应操作之后,触发这个事件,然后执行相应的事件处理函数,一般情况下,我们可以通过三种方式给元素添加事件处理 ...
1
PL/SQL loop循环详解
日志  在PL/SQL中可以使用LOOP语句对数据进行循环处理,利用该语句可以循环执行指定的语句序列.常用的LOOP循环语句包含3种形式:基本的LOOP.WHILE...LOOP和FOR...LOOP. ...
1
循环(数组循环、获取json数据循环)、each()循环详解
日志return; // 退出循环(不满足,退出此次循环.下次满足条件,依然会走此循环)return false; //退出函数(退出所有)  一. 数组循环: html: <div class=& ...
1
Shell编程之while&until循环详解
日志循环语句命令常用于执行一条指令或者一组指令,那么直到条件不在满足时停止,在shell脚本中循环语句常见有 while until for select循环语句. 在while循环语句主要用来重复执行一 ...
1
详解JavaScript中的Event Loop(事件循环)机制
日志前言 我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言.这是由其最初的用途来决定的:与浏览器交互. 单线程意味着,javascript代码在执行的任何时候,都只有一个主线程 ...
1
深入核心,详解事件循环机制
日志深入核心,详解事件循环机制 JavaScript的学习零散而庞杂,因此很多时候我们学到了一些东西,但是却没办法感受到自己的进步,甚至过了不久,就把学到的东西给忘了.为了解决自己的这个困扰,在学习的过程 ...
3
node源码详解(六) —— 从server.listen 到事件循环
日志 本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource6 本博客同步在https://cnodejs. ...
1
(转)深入核心,详解事件循环机制
日志转载地址:https://www.jianshu.com/p/12b9f73c5a4f 这个前端面试在搞事80% 应聘者都不及格的 JS 面试题 在学习事件循环机制之前,我默认你已经懂得了如下概念,如 ...
1
node.js – 如何增加nodejs中的事件循环容量?
问答我知道Node.js使用单线程和事件循环来处理请求,一次只处理一个(非阻塞).但我无法确定每秒运行100k请求的事件循环容量. 在这里,我希望nodejs服务器的容量规划能够处理每秒100k的请求. ...
1
Android中点击事件的四种写法详解
日志Android中点击事件的四种写法 使用内部类实现点击事件 使用匿名内部类实现点击事件 让MainActivity实现View.OnClickListener接口 通过布局文件中控件的属性 第一种方法 ...
2
Spring事件机制详解
日志一.前言   说来惭愧,对应Spring事件机制之前只知道实现 ApplicationListener 接口,就可以基于Spring自带的事件做一些事情(如ContextRefreshedEvent) ...
1
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
日志我们在开发组件时有时需要和父组件沟通,此时可以用自定义事件来实现 组件的事件分为自定义事件和原生事件,前者用于子组件给父组件发送消息的,后者用于在组件的根元素上直接监听一个原生事件,区别就是绑定原生事 ...
2
手写instanceof (详解原型链) 和 实现绑定解绑和派发的事件类
日志A  instanceof  B    是判断  A  是否继承自B,是返回true,  否返回false 再精确点就是判断B   是否  再  A  的 原型链上, 什么是原型链,举个例子: 我们定 ...
1
委托与事件-事件详解(二)
日志前言 上一节我们了解学习了委托,委托是类型安全的类,它定义了返回类型和参数的类型.委托类不仅包含对方法的引用,也可以包含对多个方法的引用. 理解委托的一种好的方式是把委托视为是给方法的签名和返回类型指 ...
1
委托与事件-委托详解(一)
日志前言 说起委托和事件,我就想起了再学校的时候,当时死记硬背去记什么是委托什么是事件.记得当时蝼某人问我,委托是什么?但是只知道一点点,就跟他说:打个比方,我要喝水,但是我不去买,我委托你去帮我买水.这 ...
1
事件委托或是事件代理详解
日志起因: 1.这是前端面试的经典题型,要去找工作的小伙伴看看还是有帮助的: 2.其实我一直都没弄明白,写这个一是为了备忘,二是给其他的知其然不知其所以然的小伙伴们以参考: 概述: 那什么叫事件委托呢?它 ...
2
tap事件的原理详解
日志点击事件延迟问题所在: 在移动端中,由于两次触摸是放大操作,,所以当你点击一次的时候,浏览器会等待300ms,看用户是否会进行第二次点击,如果没有的话,才会执行点击事件 为什么要解决: 随着h5游戏, ...
1
Android事件传递机制详解及最新源码分析——Activity篇
日志版权声明:本文出自汪磊的博客,转载请务必注明出处. 在前两篇我们共同探讨了事件传递机制<View篇>与<ViewGroup篇>,我们知道View触摸事件是ViewGroup传递 ...
1