之前面试的时候遇到了一个关于eventloop和asynchronoussynchronization的问题。当时,我对它的了解还不够深入。现在我拿起??它,学着理解它,以加深印象。1.概念JavaScript引擎:JavaScript引擎是一个虚拟机(进程),负责代码执行、内存分配、垃圾回收、代码编译等,一个JavaScript引擎会驻留在内存中,等待宿主(浏览器)传递JavaScript代码或执行它的功能。宏任务(macrotask):由宿主环境(浏览器)发起的任务,如setTimeout、setInterval,宏任务包括生成dom对象、解析html、执行主线程js代码、改变当前url以及页面加载等其他事件,输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一些离散的、独立的工作。执行任务后,浏览器可以继续执行其他任务,例如页面重新呈现和垃圾收集。Microtask:由JavaScript引擎发起的任务。就是完成一些更新应用状态的小任务,比如promises,updatedom操作等。其次,同步和异步JavaScript在单线程中运行,同步任务会在主线程中依次执行,但有时会使用引擎以外的功能。这时候就需要和外界进行交互,形成一个异步任务。下发同步任务时,直到得到执行结果后调用才会返回。等待调用值返回。也就是说,就是调用者主动等待调用结果的过程。异步任务是在不等待返回结果的情况下发出调用,被调用者通过状态和通知通知调用者,或者通过回调函数处理调用。2.1process.nextTick和promise.thenprocess.nextTick()在当前调用栈结束后立即处理。这时候肯定是“在事件循环继续之前”,promise.then会被放到MicroTaskQuene中等待执行,所以总是在promise.then之前输出的是process.nextTick。2.2setTimeout和setImmediate:setTimeout和setImmediate是在事件循环执行的不同阶段执行的,定时器的执行顺序会根据调用的上下文不同而不同。如果两者都是从主模块调用,输出顺序会受到进程性能的限制(这可能会受到计算机上运行的其他应用程序的影响,先抢到资源的先执行)。如:setTimeout(()=>console.log(1));setImmediate(()=>console.log(2));多次执行的结果:如果这两个定时在一个I/O周期内执行,setImmediate总是先执行,所以setImmede回调总是在setTimeout之前执行。参考NodeJs文档中的例子:constfs=require('fs');fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0);setImmediate(()=>{console.log('immediate');});});输出总是如下:三、事件循环参考NodeJs中事件循环的示意图和解释:事件循环的每个阶段都有一个FIFO队列来执行回调。虽然每个阶段都有自己的特殊方式,但一般来说,当事件循环进入给定阶段时,它将执行该阶段特定的任何操作,然后对该阶段的队列执行回调,直到队列为空或最大回调次数。当队列为空或达到回调限制时,事件循环将进入下一阶段,依此类推。由于这些操作中的任何一个都可能调度更多操作,并且在轮询阶段处理的新事件由内核排队,因此可以在处理轮询事件时将它们排队。因此,长时间运行的回调会导致轮询阶段运行的时间比计时器的阈值长得多。**注意:**Windows和Unix/Linux实现之间存在细微差别,但这对于本演示并不重要。最重要的部分在这里。其实有七八个步骤,但是我们关心的(Node.js实际用到的)就是上面这些。3.1事件循环定时器(timers)的阶段概述:这个阶段执行setTimeout()和setInterval()设置的回调。挂起的回调:I/O回调,其执行被推迟到下一个循环迭代。空闲,准备(空闲,准备):仅在内部使用。轮询(poll):获取新完成的I/O事件;在合适的时候执行I/O相关的回调(除了close回调、定时器调度的回调、setImmediate之外的几乎所有回调),node会阻塞在这里。检查:setImmediate()回调在这里执行。关闭回调:一些关闭回调,比如socket.on('close',...)。在事件循环的每次运行之间,Node.js检查它是否正在等待任何异步I/O或计时器,如果没有,则将其彻底关闭。引用阮一峰文章中的图就更清楚了:3.2。详细介绍每个阶段的定时器定时器可以指定提供回调的阈值,定时器回调会在指定的时间过去后尽快运行。但是,它们可能会因操作系统调度或其他回调的运行而延迟。注意:从技术上讲,[PollingPhase]控制定时器执行多长时间。例如,假设您计划在100毫秒的阈值后执行超时,然后脚本开始异步读取需要95毫秒的文件:constfs=require('fs');functionsomeAsyncOperation(callback){//假设读取filetakes95msfs.readFile('/path/to/file',callback);}consttimeoutScheduled=Date.now();setTimeout(()=>{constdelay=Date.now()-timeoutScheduled;console.log(`${delay}mssinceI'sscheduled`);},100);//执行someAsyncOperation需要95ms才能完成someAsyncOperation(()=>{conststartCallback=Date.now();//假设执行一些其他代码需要10毫秒...while(Date.now()-startCallback<10){}});当事件循环进入轮询阶段时,仍然是一个空队列(fs.readFile()还没有执行完),所以异步读取文件需要等待95ms,需要10ms将完成的回调添加到轮询中排队并执行。当回调完成后,队列中不再有回调,因此事件循环会看到它已经达到了快速定时器的最大阈值,然后返回定时器阶段执行定时器的回调。从上面的例子可以看出,总的延迟,也就是定时器被设置到回调被执行需要105ms。Pendingcallbacks这个阶段执行的回调是一些系统操作,比如TCP类型的错误。如果TCP套接字在尝试连接时收到ECONNREFUSED,某些*nix系统会等待报告错误。在pendingcallbacks阶段会排队等待执行。poll轮询阶段主要有两个作用:1.计算轮询和阻塞I/O的时间2.处理轮询队列中的事件当事件循环进入轮询阶段时,此时没有调度定时器。将发生以下两种情况之一:1.*如果轮询队列不为空*,事件循环将遍历其回调队列,使其同步执行,直到队列耗尽或达到系统相关的硬限制为止。2.*如果轮询队列为空,*会发生以下两种情况之一:如果脚本已通过setimmediate()调度,事件循环将结束轮询阶段并继续检查阶段以执行那些调度的脚本。如果setimmediate()没有调度脚本,事件循环将等待回调被添加到队列中,然后立即执行它们。一旦轮询队列为空,事件循环就会检查是否有达到其时间阈值的计时器。如果一个或多个计时器就绪,事件循环将返回到计时器阶段以执行这些计时器的回调。check此阶段允许一个人在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲,并且脚本对队列进行了setImmediate(),则事件循环可能会继续进入检查阶段而不是等待。setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuvAPI,它安排在轮询阶段完成后执行回调。通常,在执行代码时,事件循环最终会到达等待传入连接、请求等的轮询阶段。但是,如果回调setImmediate()已被调度并且轮询阶段变为空闲,它将结束并继续检查阶段而不是等待轮询事件。关闭回调如果套接字或句柄突然关闭(例如socket.destroy()),“关闭”事件将在此阶段发出。否则它将由process.nextTick()发出。3.3示例区1.举一个简单的例子来分析执行过程:Promise.resolve().then(()=>console.log(1));process.nextTick(()=>console.log(2));(()=>console.log(3))();setTimeout(function(){console.log(4)Promise.resolve().then(()=>console.log(5))process.nextTick(()=>console.log(6))},0);正确的输出序列:321465解析如下:1.主程序执行Promise.resolve().then(()=>console.log(1));,发现是一个microtask,放入首先是microTasks队列。2.process.nextTick(()=>console.log(2));放在nextTick队列中,在同步任务执行完之后或者事件循环进行之前立即执行。3、主程序执行到(()=>console.log(3))();发现是立即执行函数(同步任务),*先输出3*。4.接下来setTimout,主程序会把它放到MacroTasks队列中。JavaScript执行完以上步骤后,如下图所示:5.nextTick是在主程序栈中的任务执行完后立即添加的,所以then输出26。微任务总是在宏任务之前执行,所以Promise.then先执行回调,输出17。然后执行宏任务内部的回调。7.1.顺序执行输出47.2.然后将promise.then放入microTasks中等待执行7.3.process.nextTick在本次同步任务结束时额外执行,输出67.4。最后在microtask中执行promise.then,输出52。再来一个例子,这是阮一峰老师文章评论区的一个例子。想了很久,现在分析一下。如有错误请指出:setImmediate(function(){//s1console.log(1);process.nextTick(function(){//p1console.log(2);});})process.nextTick(function(){//p2console.log(3);setImmediate(function(){//s2console.log(4);})})下面按照主程序的执行顺序来分析。按顺序,第一个setImmediate回调命名为s1,第二个回调命名为s2,第一个setImmediate中的process.nextTick回调命名为p1,第二个process.nextTick命名为p2:1。首先,s1会被放在检查队列由第一轮事件循环来执行。2、因为主程序当前调用栈为空,然后执行p2,输出3,此时队列中还有setImmediate,所以在同一轮事件循环3的check阶段放入s2.按照队列先进先出的执行顺序,所以先在s1中输出1,再在s2中输出44.只有在队列中执行完回调后,即清空队列后才进入下一阶段。此时执行事件循环检查阶段的回调,最后在队列结束后加入p1,输出2;输出结果:参考文章:https://nodejs.org/en/docs/gu...https://juejin.im/post/5a62e1...https://time.geekbang.org/col。..https://jakearchibald.com/201...http://www.ruanyifeng.com/blo...
