当前位置: 首页 > 后端技术 > Node.js

JavaScript中的事件循环机制

时间:2023-04-04 00:15:09 Node.js

文章首发于个人博客前言最近面试了很多公司,这个问题几乎是必问的问题。以前总觉得自己知道的差不多,但是第一次被问到的时候不知道从何说起,涉及的知识点很多。所以花了一些时间来整理。不仅仅是因为面试,理解了JavaScript的事件循环机制,就会解答我们平时遇到的疑惑。一般面试官都会问这个问题,要求你把打印出来的结果打印出来。然后我会问说说浏览器节点的事件循环,有什么区别,什么是宏任务和微任务,为什么会有这两个任务。。。这篇文章参考了很多文章,加上自己的理解,如果有问题希望大家指出。事件循环JavaScript是单线程的,非阻塞的浏览器事件循环执行栈和事件队列宏任务和微任务节点环境和浏览器环境下的事件循环有什么区别事件循环模型宏任务和微任务经典题目分析1.JavaScript是单线程的,非阻塞的单线程:JavaScript的主要目的是与用户交互和操作DOM。如果是多线程,会有很多复杂的问题需要处理,比如两个线程同时操作DOM,一个线程删除当前DOM节点,另一个线程操作当前DOM阶段,这线程的操作是最后一个?允许?为了避免这种情况,JS是单线程的。H5虽然提出了webworker标准,但是有很多局限性,受主线程控制,是主线程的子线程。非阻塞:通过事件循环实现。2.浏览器的eventloop执行栈和事件队列为了更好的理解EventLoop,请看下图(引用自PhilipRoberts的演讲《Help, I'm stuck in an event-loop》)执行栈:同步代码的执行顺序加入到执行栈中在函数a(){b();console.log('a');}functionb(){console.log('b')}a();我们可以使用Loupe(Loupe是一个可视化工具,可以帮助你理解JavaScript的调用栈/事件循环/回调队列是如何交互的)工具来理解上面代码的执行过程。执行函数a()先入栈a(),函数b()先执行。functionb()入栈执行functionb(),console.log('b')和输出b入栈,console.log('b')popfunctionb()执行完成,popsconsole.log('a')入栈,执行,输出a,pops函数a执行完毕,出栈。事件队列:异步代码的执行,遇到异步事件不会等待它返回结果,而是暂停该事件,继续执行执行栈中的其他任务。当异步事件返回结果时,放入事件队列。事件队列不会立即执行回调,而是等待当前执行栈中的所有任务执行完毕。主线程空闲,主线程会搜索事件。队列中是否有任务,如果有,取出第一个事件,将这个事件对应的回调放入执行栈,然后执行里面的同步代码。我们在上面的代码的基础上添加异步事件,functiona(){b();console.log('a');}functionb(){console.log('b')setTimeout(function(){console.log('c');},2000)}a();此时的执行过程如下。让我们添加一个点击事件,同时查看正在运行的进程。$.on('button','click',functiononClick(){setTimeout(functiontimer(){console.log('你点击了按钮!');},2000);});console.log("你好!”);setTimeout(functiontimeout(){console.log(“点击按钮!”);},5000);console.log(“欢迎使用放大镜。”);简单用下图总结一下为什么宏任务和微任务要引入微任务,只有一种任务不行吗?页面渲染事件、各种IO完成事件等随时加入到任务队列中,永远按照先进先出的原则执行。我们无法准确控制这些事件加入任务队列的位置。但是这个时候突然有高优先级的任务需要尽快执行,所以一类任务是不合适的,所以引入了微任务队列。不同的异步任务分为:宏任务和微任务宏任务:脚本(整体代码)setTimeout()setInterval()postMessageI/OUI交互事件微任务:newPromise().then(回调)MutationObserver(html5新特性)的运行机制的异步任务的返回结果会放入一个任务队列中。根据异步事件的类型,这个事件实际上会被放到对应的宏任务和微任务队列中。当当前执行栈为空时,主线程会检查微任务队列中是否有事件,依次执行队列中事件对应的回调,直到微任务队列为空,然后再去宏任务队列中处理取出前面的事件。将当前回调添加到当前指向的堆栈。如果不存在,则去宏任务队列中取出一个事件,将对应的返回添加到当前执行栈;当前执行栈执行完毕后,立即处理微任务队列中的所有事件,然后去宏任务队列中取一个事件。在同一个事件循环中,微任务总是在宏任务之前执行。在事件循环中,每个循环操作都称为tick,每个tick的任务处理模型比较复杂,但关键步骤如下:执行一个宏任务(不在栈中则从事件队列中获取)).当遇到微任务时,将其添加到微任务的任务队列中。宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(顺序执行)。当前macrotask执行完后,开始渲染,然后GUI线程渲染完成后,JS线程继续接管,开始下一个macrotask(从事件队列中获取)。简单总结一下执行顺序:先执行宏任务,再执行宏任务生成的微任务。如果微任务在执行过程中有新的微任务产生,微任务会继续执行。microtask执行完后,会返回到macrotask进行下一次循环。深入理解js事件循环机制(浏览器篇)这篇文章有专门的动图,大家可以看一下理解一下。console.log('start')setTimeout(function(){console.log('setTimeout')},0)Promise.resolve().then(function(){console.log('promise1')}).then(function(){console.log('promise2')})console.log('end')全局代码被压入执行栈执行,输出startssetTimeout被压入macrotask队列,promise.then回调被放入microtask队列,最后console执行.log('end'),输出end调用栈中代码的执行情况(全局代码属于macrotask),然后开始执行里面的代码microtask队列,执行promise回调,输出promise1,promise回调函数默认返回undefined,promise状态变为fulfilled,触发下一个then回调,继续推入microtask队列。这时候会产生一个新的microtask,会执行当前的microtask队列。这时候执行了第二个promise.then回调。输出promise2,此时microtask队列已经清空,接下来会执行UI渲染工作(如果有的话),然后执行下一轮事件循环,执行setTimeout的回调,setTimeout的最终执行结果会输出如下。循环与浏览器环境的不同之处表现出与浏览器大致相同的状态。不同之处在于节点有自己的一套模型。node中事件循环的实现依赖于libuv引擎。Node的事件循环存在于几个阶段。如果是node10及其之前的版本,microtask会在eventloop的各个stage之间执行,也就是执行完一个stage之后,microtask队列中的task就会被执行。节点版本更新到11后,EventLoop的运行原理发生了变化。一个stage中的宏任务(setTimeout、setInterval、setImmediate)一旦执行完,微任务队列就会立即执行,与浏览器趋于一致。下面例子中的代码是按照最新的来分析的。事件周期模型────────────────────────────┐┌——————————————————————定时器│└──────────┬──────────────────────────────┘│┌──────────┴──────────────┐┐│I/O回调││└────────────┬────────────────┘│┌──────────────────────────────────────────────────────┐││空闲,准备……────────┴────────────────┐│传入:│││投票│<──连接────│└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬────────────────┘│数据等││┌────────────────────────────────────────────────────┐└──────────────────┘││检查│└──────────────────────────┘──────────────────────────────┐└──┤──┤调用回调│└──────────────────────────┘┘Incident循环的每个阶段解释事件循环在节点中的顺序外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(closecallback)-->定时器检查阶段(timer)-->I/O事件回调阶段(I/Ocallbacks)-->空闲阶段(idle,prepare)-->轮询阶段...这些阶段的大致功能如下:定时器检测阶段(timers):该阶段执行定时器队列中的回调,如setTimeout()和setInterval()。I/O事件回调阶段(I/Ocallbacks):这个阶段执行几乎所有的回调。但不包括关闭事件、定时器和setImmediate()回调。空闲阶段(idle,prepare):这个阶段只在内部使用,可以忽略轮询阶段(poll):等待新的I/O事件,node在某些特殊情况下会阻塞在这里。检查阶段(check):setImmediate()的回调会在这个阶段执行。关闭事件回调阶段(closecallbacks):例如socket.on('close',...)关闭事件的回调轮询:该阶段为轮询时间,用于等待尚未返回的I/O事件,比如服务器响应,用户移动鼠标等等。这个阶段会持续很长时间。如果没有其他异步任务需要处理(比如过期的定时器),就会停留在这个阶段,等待I/O请求返回结果。check:该阶段执行setImmediate()的回调函数。close:该阶段执行关闭请求的回调函数,如socket.on('close',...)。timerphase:这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查当前时间是否满足定时器的条件。如果满足,执行回调函数,否则离开该阶段。I/O回调阶段:除了下面的回调函数,其他的都在这个阶段执行:setTimeout()和setInterval()的回调函数setImmediate()的回调函数用于关闭请求的回调函数,比如assocket.on('close',...)macrotasks和microtasksMacrotasks:setImmediatesetTimeoutsetIntervalscript(整体代码)I/O操作等Microtask:process.nextTicknewPromise().then(callback)Promise.nextTick,setTimeout,setImmediate使用场景及区别Promise.nextTickprocess.nextTick是一个独立于eventLoop的任务队列。在每个eventLoop阶段完成后,将检查nextTick队列。如果其中有任务,这些任务将在微任务之前执行。它是所有异步任务中执行速度最快的。setTimeout:setTimeout()方法是定义一个回调,希望这个回调在我们指定的时间间隔后尽快执行。setImmediate:setImmediate()方法某种意义上会立即执行,但实际上会在固定阶段执行回调,即在poll阶段之后。经典题目分析1.下面的代码输出了什么asyncfunctionasync1(){console.log('async1start');等待async2();console.log('async1end');}asyncfunctionasync2(){console.log('async2');}console.log('scriptstart');setTimeout(function(){console.log('setTimeout');},0)async1();newPromise(function(resolve){console.log('promise1');resolve();}).then(function(){console.log('promise2');});console.log('脚本结束');先执行宏任务(当前代码块也可以看成宏任务),然后执行当前宏任务生成的微任务,再执行宏任务从上到下执行代码,先执行同步代码,输出scriptstart遇到setTimeout,现在把setTimeout代码放到宏任务队列中执行async1(),输出async1start,然后执行async2(),输出async2,把代码放到console.log('async1end')async2()进入microtask队列后执行,输出promise1,将.then()放入microtask队列;注意Promise本身是同步立即执行函数,.then是异步执行函数然后往下执行,输出scriptend。同步代码(也是宏??任务)执行完成后,接下来会执行刚才放在微任务中的代码,依次执行微任务中的代码,async1结束而promise2会依次输出。微任务中的代码执行完后,会执行宏任务中的代码输出setTimeout最终的执行结果如下scriptstartasync1startasync2promise1scriptendasync1endpromise2setTimeout2.下面的代码输出什么console.log('start');setTimeout(()=>{console.log('children2');Promise.resolve().then(()=>{console.log('children3');})},0);newPromise(函数(resolve,reject){console.log('children4');setTimeout(function(){console.log('children5');resolve('children6')},0)}).then((res)=>{console.log('children7');setTimeout(()=>{console.log(res);},0)})这道题和上面那道题的区别在于执行代码会产生很多宏任务,而每个宏任务都会生成微任务从上到下执行代码,先执行Synchronize代码,输出start遇到setTimeout,先把setTimeout的代码放入宏任务队列①然后往下执行,输出children4,遇到setTimeout,先把setTimeout的代码放到宏任务队列②,这时候.then就不会放到微任务队列了,因为resolve是放在setTimeout执行的代码里面的,代码执行完成后,会在microtask队列中查找事件,发现没有事件,于是开始执行macrotask①,即先setTimeout,输出children2,此时,Promise.resolve().then会被放入微任务队列。宏任务①中的代码执行后,会搜索微任务队列,然后输出children3;然后开始执行宏任务②,即第二次setTimeout,输出children5,然后将.then放入微任务队列中。宏任务②中的代码执行完后,会搜索微任务队列,然后输出children7,遇到setTimeout,放入宏任务队列。此时执行microtask,启动macrotask输出children6;最终执行结果如下:startchildren4children2children3children5children7children63、下面的代码输出什么?constp=function(){returnnewPromise((resolve,reject)=>{constp1=newPromise((resolve,reject)=>{setTimeout(()=>{resolve(1)},0)resolve(2)})p1.then((res)=>{console.log(res);})console.log(3);resolve(4);})}p().then((res)=>{console.log(res);})console.log('end');执行代码,Promise本身是同步立即执行函数,.then是异步执行函数。遇到setTimeout,先放入宏任务队列。遇到p1.then,会先放到微任务队列中,然后往下执行。当output3遇到p().then时,会先放到微任务队列中。,然后往下执行,输出端同步代码块执行完成后,开始执行microtask队列中的任务,先执行p1.then,输出2,再执行p().then,输出4执行完ofmicrotask完成,开始执行宏任务,setTimeout,resolve(1),但是此时p1.then已经执行完毕,此时不会输出1。最终执行结果如下:3end24上面代码中可以注释掉resolve(2),则输出1,输出结果为3end41.constp=function(){returnnewPromise((resolve,reject)=>{constp1=newPromise((resolve,reject)=>{setTimeout(()=>{resolve(1)},0)})p1.then((res)=>{console.日志(res);})console.log(3);resolve(4);})}p().then((res)=>{console.log(res);})console.log('end');3end41最后,我强烈推荐几个解释事件循环的非常好的视频:事件循环到底是什么?|菲利普·罗伯茨|JSConfEUJakeArchibald:InTheLoop-JSConf.AsiaReferenceJavaScript中的事件循环(EventLoop)机制详解JavaScript运行机制详解:重温EventLoopNodeTimer详解面试题:谈谈事件循环机制(全评分答案来了)极客浏览器工作原理与实践MicroTasks,macrotasks,Event-LoopNode.js事件循环和JS浏览器环境下事件循环的区别图解理解JavaScript引擎EventLoop有什么区别浏览器和Node的事件循环(EventLoop)?深入理解js事件循环机制(Node.js篇)《一文看懂浏览器事件循环》LoupeOthers最近推出了100天的前端进阶计划,主要是深入挖掘每个知识点背后的原理,欢迎关注微信公众号《牧马之星》,一起学习,签到100天。