手机版

JS和Node.js中事件循环的案例分析

时间:2021-09-02 来源:互联网 编辑:宝哥软件园 浏览:

这两天和同事同事讨论了一个问题,js中的事件循环,导致了在chrome和node中运行setTimeout和Promise的程序时执行结果不一样的问题,导致了Nodejs的事件循环机制。录下来感觉挺有收获的。

console.log(1)setTimeout(函数(){ new Promise(函数(解析,拒绝){ console.log(2) resolve() })。然后(()={ console.log(3) })},0)settimeout(function(){ console . log(4)},0)//运行在chrome:1 2 3 4//运行在Node: 1 2 4 3chrome和Node有不同的结果,很有意思。

1.js中的任务队列

JavaScript语言的特点之一是单线程,这意味着一次只能做一件事。那么为什么JavaScript不能有多线程呢?这样可以提高效率。

JavaScript的单线程与其用途有关。作为一种浏览器脚本语言,JavaScript主要用于与用户交互和操作DOM。这就决定了它只能是单线程,否则会带来复杂的同步问题。例如,假设JavaScript同时有两个线程,一个线程向DOM节点添加内容,另一个线程删除节点。浏览器应该以哪个线程为标准?

因此,为了避免复杂性,JavaScript从诞生之日起就一直是单线程的,这已经成为这种语言的核心特性,未来也不会改变。

为了利用多核CPU的计算能力,HTML5提出了Web Worker标准,允许JavaScript脚本创建多个线程,但子线程完全由主线程控制,无法操作DOM。因此,这个新标准并没有改变JavaScript单线程的本质。

2.任务队列事件循环

单线程意味着所有任务都需要排队,上一个任务完成后,下一个任务才会执行。如果前一个任务需要很长时间,那么后一个任务将不得不一直等待。

因此,所有的任务可以分为两种类型,一种是同步的,另一种是异步的。同步任务是指在主线程上排队等待执行的任务,在执行下一个任务之前,只能执行上一个任务;异步任务是指不进入主线程但进入“任务队列”的任务。只有当“任务队列”通知主线程可以执行异步任务时,任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为没有异步任务就可以认为是异步执行。)

所有同步任务都在主线程上执行,形成执行上下文堆栈。主线之外还有一个“任务队列”。只要异步任务有运行结果,事件就会被放入“任务队列”。一旦“执行栈”中的所有同步任务被执行,系统将读取“任务队列”以查看其中有什么事件。那些对应的异步任务结束等待状态,进入执行栈,开始执行。主线程重复上面的第三步。

只要主线程为空,就会读取‘任务队列’,这是JavaScript的运行机制。这个过程将会重复。

3.计时器设置超时和设置间隔

定时器功能主要由setTimeout()和setInterval()完成。它们的内部运行机制完全相同,不同的是前者指定的代码执行一次,而后者重复执行。

setTimeout(fn,0)的含义是指定一个任务应该在主线程最早可用的空闲时间执行,即越早越好。它在“任务队列”的末尾添加了一个事件,因此在同步任务和“任务队列”中的现有事件处理完毕之前,它不会被执行。

按照HTML5标准,setTimeout()第二个参数的最小值(最短间隔)不得小于4毫秒,如果低于这个值,就会自动增加。在此之前,在旧浏览器中最短的时间间隔被设置为10毫秒。此外,对于那些DOM更改(尤其是涉及页面重新呈现的更改),它们通常不会立即执行,而是每16毫秒执行一次。requestAnimationFrame()的效果比setTimeout()好。

需要注意的是,setTimeout()只在‘任务队列’中插入事件,主线程在执行当前代码(执行栈)之前不会执行它指定的回调函数。如果当前代码需要很长时间,可能需要等待很长时间,因此不能保证回调函数会在setTimeout()指定的时间执行。

4.节点的事件循环

事件轮询主要用于轮询事件队列。事件生成器将事件排队到队列中。在队列的另一端,有一个名为event consumer的线程,它会不断地查询队列中是否有事件。如果有事件,会立即执行。为了防止阻塞操作影响当前线程在执行期间读取队列,事件消费者线程将委托线程池来执行这些阻塞操作。

Javascript前端和Node.js的机制类似于这个事件轮询模型。有人认为Node.js是单线程,也就是事件消费者不断被单线程轮询。有阻塞操作怎么办,是不是阻塞了当前单线程的执行?

其实Node.js的底部还有一个线程池,专门用来执行各种阻塞操作,这样就不会影响到作为主线程的单线程轮询队列中的事件,执行一些任务。在线程池操作之后,它将充当事件生成器,将操作结果放入同一个队列。

总之,事件轮询事件循环需要三个组件:

事件队列(Event Queue)属于FIFO模型,一端推入事件数据,另一端拉出事件数据,两端只通过这个队列进行通信,是异步松耦合。队列的读取轮询线程、事件的使用者和事件循环的主角。线程池(Thread Pool)是一个独立的线程池,专门用于执行长任务、重任务和重手工。

Node.js也是单线程的Event Loop,但是它的运行机制和浏览器环境不同。

根据上图,Node.js的运行机制如下。

V8引擎解析JavaScript脚本。解析后的代码调用节点应用编程接口。libuv库负责执行节点应用编程接口。它将不同的任务分配给不同的线程,形成一个Event Loop,并将任务的执行结果以异步方式返回给V8引擎。然后,V8引擎将结果返回给用户。

我们可以看到node.js的核心其实是libuv库。这个库是c写的,可以使用多线程技术,而我们的Javascript应用是单线程的。

Nodejs的异步任务执行流程:

用户写的代码是单线程的,但是nodejs不是单线程的!

事件机制:

Node.js不是使用多个线程来为每个请求执行工作,而是将所有工作添加到一个事件队列中,然后有一个单独的线程来循环提取队列中的事件。事件循环线程获取事件队列中的顶部条目,执行它,然后获取下一个条目。当执行长时间运行或阻塞输入/输出的代码时

在Node.js中,由于只有一个单线程在不停地轮询队列中是否有事件,所以数据库文件系统等I/O操作,包括HTTP请求,容易阻塞等待,肯定会阻塞其他工作任务的执行。Javascript/Node.js将委托底层线程池执行,并告诉线程池一个回调函数。这样,单个线程继续执行其他事情。当这些阻塞操作完成时,结果将与提供的回调函数一起放入队列。当单个线程从队列中读取事件并读取阻塞的操作结果时,它会将这些操作结果作为回调函数的输入参数,然后激活回调函数。

请注意,Node.js的这个单线程不仅负责读取队列事件,还负责执行回调函数,这是区别于多线程模式的一个主要特性。在多线程模式下,单个线程只负责读取队列事件,不做其他事情,会委托其他线程做其他事情。尤其是多核的情况下,一个CPU核负责读取队列事件,一个CPU核负责执行激活的任务,最适合消耗CPU计算的任务。相反,节点的执行和激活任务.js,也就是回调函数中的任务,仍然在负责轮询的单线程中执行,注定无法执行CPU的繁重任务,比如JSON转换成其他数据格式,会影响事件轮询的效率。

5.Nodejs特性

NodeJS的显著特点是异步机制和事件驱动。

事件轮询的整个过程不阻断新用户的连接,也不需要维护连接。基于这个机制,理论上用户一个接一个的请求连接,NodeJS可以响应,所以NodeJS可以支持比Java和php程序更高的并发性。

虽然维护事件队列也需要成本,但由于NodeJS是单线程的,事件队列越长,响应时间越长,并发性仍然不足。

RESTful API是NodeJS最理想的应用场景,可以处理上万个连接。它本身没有太多的逻辑,只需要请求API,组织数据返回即可。

6.例子

看一个具体的例子:

console . log(' 1 ')setTimeout(function(){ console . log(' 2 ')new Promise(function(resolve){ console . log(' 4 ')resolve()})。然后(function(){ console . log(' 5 ')})setTimeout(()={ console . log('哈哈')})new Promise(function(resolve){ console . log(' 6 ')resolve()})。然后(function(){ console . log(' 66 ')})})setTimeout(function(){ console . log('呵呵')},0)new Promise(function(resolve){ console . log(' 7 ')resolve()})。然后(function(){ console . log(' 8 ')})setTimeout(function(){ console . log(' 9 ')new Promise(function(resolve){ console . log(' 11 ')resolve()})。然后(function(){ console . log(' 12 ')})})新Promise(function(resolve){ console . log(' 13 ')resolve()})。然后(function(){ console . log(' 14 ')})//node 1 : 1,7,1 3,8,14,2,4,6,呵呵,9,11,5,66,12,哈哈//不稳定结果//node2 : 1,7,13,8,14,2,4,6,呵呵,5,66,9,11,12,哈哈//不稳定结果//node 3 3360 1,7,13,8,14

Chrome运行稳定,而在节点环境下运行不稳定。可能有两种情况。

chrome运行的原因是Promise和process.nextTick()的微任务事件队列比普通的宏任务事件队列有更高的权限。如果在事件队列中取事件时有微任务,则先执行微任务队列中的任务,除非任务在下一轮Event Loop中,然后在微任务队列清空后再执行宏任务队列中的任务。

版权声明:JS和Node.js中事件循环的案例分析是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。