资讯专栏INFORMATION COLUMN

Node中的事件循环

lwx12525 / 1502人阅读

摘要:的事件循环一个线程有唯一的一个事件循环。索引就是指否还有需要执行的事件,是否还有请求,关闭事件循环的请求等等。先来看一下定义的定义是在事件循环的下一个阶段之前执行对应的回调。虽然是这样定义的,但是它并不是为了在事件循环的每个阶段去执行的。

Node中的事件循环

如果对前端浏览器的时间循环不太清楚,请看这篇文章。那么node中的事件循环是什么样子呢?其实官方文档有很清楚的解释,本文先从node执行一个单文件说起,再讲事件循环。

node的内部模块

任何高级语言的存在都有一定的执行环境,比如浏览器的代码是在浏览器引擎中,那么在node环境中也有一定的执行环境。我们先来看一下官网的依赖包有哪些?

V8

libuv

http-parser

c-cares

OpenSSL

zlib

上面就是nodejs中依赖的模块。那么这些模块之间是如何工作的呢?模块之间的工作关系如下图所示:


主要过程如下:

step1: 用户的代码通过v8引擎解释器,解析为两部分:"立即执行"和"异步执行"。

立即执行:可以理解为,需要v8引擎去处理的代码;
异步执行:并不是真正的异步,可以理解为,不需要v8引擎处理的和需要异步处理的。

step2: “异步执行”的部分,通过v8引擎和底层之间建立的绑定关系,去执行对应的操作

step3: 在“异步执行”部分,通过libuv内部的事件循环机制,无阻塞调用。libuv在执行的时候,主要通过handles和request实现对应的操作,handles和requests具备不同的数据结构。官网解释,handles是长期存在的对象,request是短期存在的对象,猜测来讲,requests和handles有不同的垃圾回收机制。

libuv的事件循环
一个线程有唯一的一个事件循环(event loop)。线程非安全。

这里需要理解两点:

线程

这可能和我们理解的不太一样,Javascript代码是单线程的,但是libuv不是单线程的,他可以开启多个线程,libuv 提供了一个调度的线程池,线程池中的线程数目,默认是4个,最多1024个(为什么?因为每一个线程都会占用资源,而内存是有限的),关于线程池的可以看官方文档。

线程安全

对数据的操作无非就是读和写,线程安全,简单来说,就是一个线程对这一份数据具有独占性,只有当该线程操作完成,其他线程才可以进行操作,当然线程安全的概念远不止这些,详细可以看维基百科,这里就简单理解一下就行了。

libuv中的事件循环

事件循环图,如下所示:

主要分为下面几步:

step1: 线程启动时,初始化一个时间:now,为了计算后面的timer的回调函数什么时候执行

step2: 判断事件循环是否存活,如果不存活,立即退出,否则进行下一步。判断是否存活的依据:索引是否存在。索引就是指否还有需要执行的事件,是否还有请求,关闭事件循环的请求等等。(用白话来讲,就是看还有没有没处理的事情)

step3: 执行所有的定时器(timers)在事件循环之前

step4: 执行待执行(pending)的回调,一般的IO轮询都会在轮询后,立即执行,但是有的也会延迟(defer)执行,延迟执行的,就会在这个阶段执行

step4: 执行空闲(idle)函数,每个阶段都会执行的,一般情况下是执行一些必要的操作,程序内置的

step5: 执行准备好的回调函数,具体内部使用的

step6: IO轮询执行,直到超时,在阻塞执行之前,会计算超时时间,也就是停止轮询的时间:

如果队列为空、或者是即将关闭,或者有将要关闭的handles,timeout为0

如果没有上面的情况,超时时间就取最近的timer时间,否则就是无穷大

(用白话来理解,就是看有没有要关闭的,有的话,就直接往下走,没有的话,看看有哪个事件比较急,到了点就去执行)

step7: 执行IO

step8: 检查接下来要执行哪些handle,保证正确执行

step9: 是否存在关闭的回调,如果有就执行,关闭循环,否则继续循环

通常情况下来讲,文件的I/O会调用线程池,但是网络请求的I/O总是用同一个线程。

Node中的事件循环 阻塞和非阻塞

node中所有的代码几乎都提供了同步(阻塞)和异步(非阻塞)的方式,你可以选择使用哪一种方式,但是不要混合使用。

node中的事件循环,就是一个简版的libuv事件循环机制图

NodeJs中的定时器

NodeJs中的定时器主要有三种:

setTimeout

setInterval

setImmediate

三个定时器都有对应的取消函数:

clearTimeout

clearInterval

clearImmediate

setTimeout && setInterval

setTimeout和setInterval行为和在浏览器环境中的行为类似,但是setTimeout和setImmediate有一点不同。在libuv中可以看到,判断循环是否结束的时候,是需要判断是否还有待执行的函数,如果只剩下一个setTimeout或者setInterval函数,那么整个循环还会继续存在,node提供了一个函数,可以让循环暂时休眠

unref

ref

unref是可以让setTimeout暂时休眠,ref可以再次唤醒

setImmediate

setImmediate是指定在事件循环结束执行的。主要发生在poll阶段之后

如果poll队列没空,则一直执行,直到对列空位置

如果poll队列空了,有setImmediate事件,则会跳到check阶段

如果poll队列空了,没有setImmediate事件,就会查看哪一个timer事件快要到期了,转到timers阶段

依据上面的解释,就有了setTimeout和setImmediate执行先后顺序的问题:

setTimeout(() => {
  console.log("timeout");
})
setImmediate(() => {
  console.log("immediate);
});

先说答案:

可能会有两种情况:
timeout
immediate
或者
immediate
timeout

为什么?
主要是setTimeout在前或者后的问题,依赖于线程的执行速度。
主要是两个阶段:

1、v8引擎执行环境扫描代码,启动事件循环,当走到setTimeout的时候,会将timeout丢进libuv事件队列中

2、v8引擎继续执行,走到setImmediate

此时,上面的libuv事件队列可能执行第一次,刚走到poll阶段,那么接下来就会打印immediate,

也可能libuv事件队列,已经第二次循环,经过了poll阶段,然后判断timeout到时间了,去执行timeout了,这样就会先打印timeout然后再打印immediate

所以根本原因是在于事件循环执行了一次还是两次。

那我们接下来看看事件循环的逻辑

nextTick

Node添加了这样一个API,这个并不在事件循环的机制内,但是和时间循环机制相关。先来看一下定义:

nextTick的定义是在事件循环的下一个阶段之前执行对应的回调。

虽然nextTick是这样定义的,但是它并不是为了在事件循环的每个阶段去执行的。
主要有下面两种应用场景:

作为下一个执行阶段的钩子,去清理不需要的资源,或者再次请求

等运行环境准备好之后,再去执行回调

案例一:
let bar;

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

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

bar = 1;

// 输出
undefined
1

输出undefine的情况是,因为执行函数的时候,bar并没有被赋值,而process.nextTick则能保证整个执行环境都准备好了再去执行

案例二:
const server = net.createServer();
server.on("connection", (conn) => { });

server.listen(8080);
server.on("listening", () => { });

当v8引擎执行完代码后,listen的回调会直接命中poll阶段,那么server的connect事件就不会执行

案例三:

想要在构造函数中,去发送对应的事件,因为此时v8引擎还没有扫描到,而构造函数的代码会立即执行,就需要nextTick

const EventEmitter = require("events");
const util = require("util");

function MyEmitter() {
  EventEmitter.call(this);
  // 这样操作无效
  this.emit("event");
  // 应该这样
  // process.nextTick(() => {
    this.emit("event");
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on("event", () => {
  console.log("an event occurred!");
});
总结

上面三个案例,重点在于v8引擎是单线程立即执行,而libuv则是异步执行,想要在异步循环之前执行一些操作就需要process.nextTick

参考文档

Node官网解释
libuv的设计
关于libuv的概念详细解释
libuv线程池实现
并发

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106971.html

相关文章

  • [译]事件循环Node.js背后的核心概念

    摘要:事件处理器,则是当指定事件触发时,执行的一段代码。事件循环以一个无限循环的形式启动,存在于二进制文件里函数的最后,当没有更多可被执行的事件处理器时,它就退出。 前言 如果你了解过Node.js,那么你一定听说过事件循环。你一定想知道它为什么那么特殊,并且为什么你需要关注它?此时此刻的你,可能已经写过许多基于Express.js的后端代码,但没有接触到任何的循环。 在下文中,我们会先在一...

    Meils 评论0 收藏0
  • 浏览器和Node不同的事件循环(Event Loop)

    摘要:浏览器中与中事件循环与执行机制不同,不可混为一谈。浏览器环境执行为单线程不考虑,所有代码皆在执行线程调用栈完成执行。参考文章强烈推荐不要混淆和浏览器中的强烈推荐中的模块强烈推荐理解事件循环一浅析定时器详解 注意 在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同。在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同。在 ...

    haitiancoder 评论0 收藏0
  • Node.js 指南(不要阻塞事件循环或工作池)

    摘要:为什么要避免阻塞事件循环和工作池使用少量线程来处理许多客户端,在中有两种类型的线程一个事件循环又称主循环主线程事件线程等,以及一个工作池也称为线程池中的个的池。 不要阻塞事件循环(或工作池) 你应该阅读这本指南吗? 如果你编写的内容比简短的命令行脚本更复杂,那么阅读本文应该可以帮助你编写性能更高、更安全的应用程序。 本文档是在考虑Node服务器的情况下编写的,但这些概念也适用于复杂的N...

    hatlonely 评论0 收藏0
  • Node中的事件循环和异步API

    摘要:异步在中,是在单线程中执行的没错,但是内部完成工作的另有线程池,使用一个主进程和多个线程来模拟异步。在事件循环中,观察者会不断的找到线程池中已经完成的请求对象,从中取出回调函数和数据并执行。 1. 介绍 单线程编程会因阻塞I/O导致硬件资源得不到更优的使用。多线程编程也因为编程中的死锁、状态同步等问题让开发人员头痛。Node在两者之间给出了它的解决方案:利用单线程,远离多线程死锁、状态...

    atinosun 评论0 收藏0
  • 【新手向】Node.js事件循环中的:Macrotask 与 Microtask

    摘要:一句话解释在事件循环机制中,有任务两个队列队列和队列。设置任务为目前运行的任务,并执行。应该是考虑到了这一点,至少任务中的任务,是被设置了在一个事件循环中的最大调用次数的,叫。参考材料理解事件循环 在Node学习过程中,不可避免的需要对事件循环机制做深入理解,其中Macrotask(大型任务)和Microtask(小型任务)比较令人困惑,在一番google之后,我发现了几篇资料能比较好...

    CoderDock 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<