作者:DillionMegida高效处理异步操作的重要技术。在本文中,我们将深入探讨Node.js中的队列:它们是什么、它们如何工作(通过事件循环)以及它们的类型。Node.js中的队列是什么?队列是Node.js中用于组织异步操作的数据结构。这些操作以不同的形式存在,包括HTTP请求、读取或写入文件操作、流等。在Node.js中处理异步操作非常具有挑战性。根据网络质量,HTTP请求期间可能会出现不可预测的延迟(或者更糟的是,没有结果)。尝试使用Node.js读写文件时也可能会出现延迟,具体取决于文件的大小。与计时器和许多其他操作一样,异步操作完成的时间可能是不确定的。对于这些不同的延迟场景,Node.js需要能够高效地处理所有这些场景。Node.js无法处理基于先开始先处理(first-start-first-handle)或先完成先处理(first-finish-first-handle)的操作。无法做到这一点的一个原因是一个异步操作可能还包含另一个异步操作。为第一个异步进程腾出空间意味着必须先完成内部异步进程,然后才能考虑队列中的其他异步操作。有很多情况需要考虑,所以最好的办法是制定规则。此规则会影响事件循环和队列在Node.js中的工作方式。让我们简单了解一下Node.js是如何处理异步操作的。调用堆栈、事件循环和回调队列调用堆栈用于跟踪当前正在执行的函数以及它开始运行的位置。当一个函数即将执行时,它被添加到调用堆栈中。这有助于JavaScript在函数执行后追溯其处理步骤。回调队列是在后台操作完成时将回调函数作为异步操作保存的队列。它们以先进先出(FIFO)的方式工作。我们将在本文后面介绍不同类型的回调队列。请注意,Node.js会处理所有异步活动,因为JavaScript可以利用其单线程特性来防止产生新线程。它还负责在后台操作完成后向回调队列添加函数。JavaScript本身与回调队列无关。同时,事件循环会不断地检查调用栈是否为空,从而从回调队列中提取一个函数加入到调用栈中。事件循环仅在执行完所有同步操作后才检查队列。那么,事件循环是按照什么顺序从队列中选择回调函数的呢?首先,让我们看一下回调队列的五种主要类型。回调队列的类型IO队列(IOqueue)IO操作是指涉及外部设备(如电脑硬盘、网卡等)的操作。常见的操作包括读写文件操作、网络操作等,这些操作应该是异步的,交给Node.js来处理。JavaScript无法访问您计算机的内部设备。执行此类操作时,JavaScript将其传输到Node.js以在后台处理。完成后,它们将被转移到IO回调队列中,以便事件循环转移到调用堆栈中执行。定时器队列(Timerqueue)每一个涉及Node.js定时器函数(例如setTimeout()和setInterval())的操作都会被添加到定时器队列中。请注意,JavaScript语言本身没有计时器功能。它使用Node.js提供的定时器API(包括setTimeout)来执行与时间相关的操作。所以定时器操作是异步的。无论是2秒还是0秒,JavaScript都会将与时间相关的操作交给Node.js,然后由Node.js完成并添加到定时器队列中。例如:setTimeout(function(){console.log('setTimeout');},0)console.log('yeah')#returnyeahsetTimeout在处理异步操作的同时,JavaScript会继续执行其他操作。只有在处理完所有同步操作后,事件循环才会进入回调队列。Microtask队列这个队列分为两个队列:第一个队列包含被process.nextTick函数延迟的函数。事件循环执行的每次迭代称为一个滴答。process.nextTick是一个函数,它在下一个时间点(即事件循环的下一次迭代)执行一个函数。微任务队列需要存储这样的函数,以便它们可以在下一个滴答时执行。这意味着事件循环必须在进入其他队列之前不断检查微任务队列中是否存在此类函数。第二个队列包含由承诺延迟的功能。可以看到,在IO和定时器队列中,所有与异步操作相关的都交给了异步函数。但承诺是不同的。在Promise中,初始变量存储在JavaScript内存中(您可能已经注意到)。异步操作完成后,Node.js将函数(附加到Promise)放在微任务队列中。同时,它用结果更新JavaScript内存中的一个变量,这样函数就不会以运行。以下代码说明了promise的工作原理:letprom=newPromise(function(resolve,reject){//延迟执行setTimeout(function(){returnresolve("hello");},2000);});控制台日志(舞会);//Promise{}prom.then(function(response){console.log(response);});//2000ms后,输出//hello关于microtask队列,需要注意的一个重要特性,事件循环在进入其他队列之前,会反复检查并执行microtask队列中的函数。例如,当一个微任务队列完成,或者一个定时器操作实现了一个承诺,事件循环将在继续执行定时器队列中的其他功能之前提交该承诺。因此,微任务队列比其他队列具有最高的优先级。校验队列(Checkqueue)校验队列也叫立即队列(immediatequeue)。IO队列中的回调函数全部执行完毕后,立即执行该队列中的回调函数。setImmediate用于向该队列添加函数。例如:constfs=require('fs');setImmediate(function(){console.log('setImmediate');})//假设这个操作需要1msfs.readFile('path-to-file',function(){console.log('readFile')})//假设这个操作耗时3msdo...while...在执行这个程序的时候,Node.js在check队列中添加了setImmediate回调函数。由于整个程序还没有准备好,事件循环不检查任何队列。因为readFile操作是异步的,所以会交给Node.js处理,之后程序会继续执行。dowhile操作持续3ms。在此期间,readFile操作完成并被推送到IO队列。完成后,事件循环将开始检查队列。虽然check队列先被填满,但是直到IO队列空了才考虑使用。所以在setImmediate之前,readFile被输出到控制台。关闭队列(Closequeue)这个队列存储了与关闭事件操作相关的函数。包含以下内容:流关闭事件,在流关闭时发出。它表示将不再发出任何事件。http关闭事件,在服务器关闭时发出。这些队列被认为是最低优先级的,因为这里的操作将在稍后发生。你绝对不希望回调函数在promise函数被resolve之前的close事件中执行。当服务器关闭时,promise函数会做什么?队列顺序微任务队列具有最高优先级,其次是定时器队列、I/O队列、检查队列,最后是关闭队列。回调队列的例子让我们通过一个更复杂的例子来说明队列的类型和顺序:constfs=require("fs");//假设这个操作需要2msfs.writeFile('./new-file.json','...',function(){console.log('writeFile')})//假设这需要10ms来完成fs。readFile("./file.json",function(err,data){console.log("readFile");});//不用假设,这其实需要1mssetTimeout(function(){console.log("setTimeout");},1000);//假设这个操作需要3mswhile(...){...}setImmediate(function(){console.log("setImmediate");});//它需要4个mslet解决承诺promise=newPromise(function(resolve,reject){setTimeout(function(){returnresolve("promise");},4000);});promise.then(function(response){console.log(响应)})console.log("最后一行");程序流程如下:0毫秒,程序启动。在Node.js将回调函数添加到IO队列之前,fs.writeFile在后台花费了2毫秒。在Node.js将回调函数添加到IO队列之前,fs.readFile在后台需要10毫秒。在Node.js将回调函数添加到IO队列之前,fs.readFile在后台需要10毫秒。setTimeout在Node.js将回调函数添加到计时器队列之前在后台花费1ms。现在,while操作(同步)需要3毫秒。在此期间,线程被阻塞(记住JavaScript是单线程的)。同样在这段时间内,setTimeout和fs.writeFile操作完成并分别将它们的回调函数添加到定时器和IO队列中。队列现在是://queuesTimer=[function(){console.log("setTimeout");},];IO=[函数(){console.log("writeFile");},];setImmediate会将回调函数添加到Check队列中:js//queueTimer...IO...Check=[function(){console.log("setImmediate")}]在promise操作之前需要4ms添加到微任务队列时间以在后台解析。最后一行是同步的,所以会立即执行:#Return"lastline"由于所有同步活动都已完成,事件循环开始检查队列。由于微任务队列是空的,它从定时器队列开始://queueTimer=[]//nowemptyIO...Check...#return"lastline""setTimeout"wheneventloopcontinuesexecution当回调队列中的函数,promise操作完成并添加到微任务队列中://QueueTimer=[];Microtask=[function(response){console.log(response);},];IO=[];//当前为空Check=[];//当前在IO后面,空#results"lastline""setTimeout""writeFile""setImmediate"几秒后,readFile操作完成,加入到IO队列中://queueTimer=[];微任务=[];//当前为空IO=[function(){console.log("readFile");},];检查=[];#results"lastline""setTimeout""writeFile""setImmediate""promise"最后,执行所有回调函数://queueTimer=[]Microtask=[]IO=[]//nowemptyagainCheck=[];#results"lastline""setTimeout""writeFile""setImmediate""promise""readFile"这里要注意三点:异步操作依赖于加入队列前的延迟。它不依赖于它们在程序中的存储顺序。在继续检查其他任务之前,事件循环在每次迭代中不断检查微任务队列。即使后台有另一个IO操作(readFile),事件循环也会执行检查队列的函数。原因是此时IO队列为空。请记住,检查队列回调将在执行完IO队列中的所有函数后立即运行。总结JavaScript是单线程的。每个异步函数都由Node.js处理,它依赖于操作系统内部来工作。Node.js负责将回调函数(通过JavaScript附加到异步操作)添加到回调队列。事件循环决定在每次迭代中接下来将执行哪个回调函数。了解队列在Node.js中的工作方式可以让您更好地理解它们,因为队列是环境的核心功能之一。Node.js最流行的定义是非阻塞(non-blocking),意思是可以正确处理异步操作。这一切都是因为事件循环和回调队列使这个功能起作用。扫码进入前端面试星球?,解锁刷题神器,领取800+前端面试真题和一线面试常用高频考点。