当前位置: 首页 > 科技观察

为什么浏览器和Node.js的EventLoop是这样设计的?

时间:2023-03-12 07:14:07 科技观察

事件循环是JavaScript的基本概念。是面试必问的话题,也常被提及,但你有没有想过为什么会有EventLoop,为什么要这样设计?今天我们就来探究其中的原因。浏览器的EventLoopJavaScript用于实现网页交互逻辑,涉及dom操作。如果多个线程同时操作,需要同步互斥。为了简单起见,设计成单线程,但是如果是单线程,遇到时序逻辑,网络请求又会被阻塞。该怎么办?您可以添加一层调度逻辑。将JS代码一个一个封装成任务,放到一个任务队列中,主线程会不断的去取任务并执行。每次执行提取任务时,都会创建一个新的调用堆栈。其中,定时器和网络请求实际上是在其他线程中执行的。执行完后,在任务队列中放入一个任务,告诉主线程可以继续执行了。因为这些异步任务是在其他线程中执行的,然后通过任务队列通知主线程,这是一种事件机制,所以这个循环称为EventLoop。这些在其他线程中执行的异步任务包括定时器(setTimeout、setInterval)、UI渲染、网络请求(XHR或fetch)。但是,当前的EventLoop存在严重的问题。没有优先级的概念,只按顺序执行。如果有高优先级的任务,则不会及时执行。因此,必须设计一种队列跳转机制。然后只需创建一个高优先级任务队列即可。每执行一次普通任务,先执行所有高优先级任务,然后再执行普通任务。通过插队机制,可以及时执行高质量的任务。这是当前浏览器的事件循环。其中,普通任务称为MacroTasks(宏任务),高质量任务称为MicroTasks(微任务)。宏任务包括:setTimeout、setInterval、requestAnimationFrame、Ajax、fetch、script标签代码。微任务包括:Promise.then、MutationObserver、Object.observe。如何理解宏观任务和微观任务的划分?定时器和网络请求是常见的异步逻辑,在其他线程运行完毕后通知主线程,所以都是宏任务。三类优质任务也很好理解。MutationObserver和Object.observe都监控一个对象的变化。变化是非常瞬间的。必须立即响应,否则可能会再次更改。Promise是组织异步Process,然后异步endcall也很好。这是浏览器中EventLoop的设计:Loop机制和Task队列是为了支持异步和解决逻辑执行阻塞主线程的问题而设计的,MicroTask队列的队列插入机制是为了解决早期执行高质量任务的问题。但是后来,JS的执行环境不仅仅是浏览器,还有Node.js,同样需要解决这些问题,只是它设计的EventLoop更加细致。Node.js的事件循环Node.js是一个新的JS运行环境。它还支持异步逻辑,包括定时器、IO和网络请求。显然,也可以使用EventLoop来运行。但是,浏览器的事件循环是为浏览器设计的。对于高性能服务器来说,这样的设计还是有些粗糙。粗糙度在哪里?浏览器的EventLoop只分为两种优先级,一种是宏任务,一种是微任务。但是宏任务之间没有优先级,微任务之间也没有优先级。Node.js任务宏任务也有优先级。比如timer定时器的逻辑,优先级高于IO的逻辑,因为在时间上,越早越准确;并且关闭资源的处理逻辑优先级非常高。low,因为不关闭,最多占用内存等资源,影响不大。所以宏任务队列被分成五个优先级:Timers、Pending、Poll、Check、Close。解释一下这五个宏任务:TimersCallback:说到时间,一定要更早更准确地执行,所以很容易理解这个优先级是最高的。PendingCallback:处理网络、IO等异常时的回调。有些*niux系统会等待报错,所以必须处理。PollCallback:处理IO数据和网络连接。这是服务器主要处理的事情。CheckCallback:执行setImmediate的回调,特点是在IO刚刚执行完后可以回调。CloseCallback:关闭资源的回调,不会被后面的执行影响,优先级最低。所以,Node.js的EventLoop是这样工作的:还有一个不同点需要特别注意:Node.js的EventLoop不像浏览器,一次执行一个宏任务,然后执行所有microtasks,而是执行一定数量的Timersmacrotasks后,执行所有microtasks,然后执行一定数量的Pendingmacrotasks,再执行所有microtasks。其余的Poll、Check和Close宏任务也是如此。为什么是这样?其实从优先级上来说很容易理解:假设宏任务在浏览器中的优先级为1,那么它是按顺序执行的,即一个宏任务,所有微任务,然后一个宏任务,然后所有微任务。Node.js的宏任务也是有优先级的,所以Node.js的EventLoop每次运行微任务之前先运行当前优先级的所有宏任务,然后再运行下一个优先级。宏任务。即一定数量的Timersmacrotasks,所有的microtasks,一定数量的PendingCallbackmacrotasks,以及所有的microtasks。为什么是一定数量?因为如果某个阶段的宏任务太多,下一个阶段就永远执行不完了,所以有一个上限,下一个EventLoop剩下的会继续执行。除了宏任务的优先级,微任务也有优先级,另外还有一个高优先级的微任务process.nextTick,它运行在所有普通微任务之前。因此,Node.jsEventLoop的完整流程如下:Timers阶段:执行一定数量的定时器,也就是setTimeout和setInterval回调,如果太多,留到下一次执行microtasks:全部执行nextTickmicrotasks,然后执行其他普通的microtasksPendingstage:执行一定量的IO和网络异常回调,如果太多,保存起来留到下次执行microtasks:执行完所有的nextTickmicrotasks,然后执行其他普通的microtasksIdle/PrepareStage:供内部使用的阶段。Microtask:执行所有nextTickmicrotasks,然后再执行其他普通的microtasks。Poll阶段:执行一定数量的文件数据回调和网络连接回调。如果太多,保存起来,下次执行。如果没有IO回调,timers和check阶段也没有回调需要处理,阻塞在这里等待IO事件setImmediatecallbacks,太多话留给下次执行。Microtask:执行所有nextTickmicrotasks,然后再执行其他普通的microtasks。close阶段:执行一定数量的close事件回调,如果数量过多,保存下来,下次执行。Microtasks:执行所有nextTickmicrotasks,然后再执行其他普通的microtasks。和浏览器中的EventLoop相比,显然要复杂很多,但是经过我们前面的分析,我们也可以了解到:Node.js做了一个macrotask的优先级划分,从高到低,分别是Timers,Pending,Poll,Check,把这五种合上,也分出了microtasks,也就是nextTick的microtasks和其他的microtasks。执行流程是先执行一定数量的当前优先级的macrotasks(剩下的留给下一次循环),然后执行process.nextTick的microtasks,再执行普通的microtasks,再执行一定数量的macrotasks..这个循环继续。Node.js内部逻辑还有一个Idle/Prepare阶段,不需要关心。改变了浏览器EventLoop一次执行一个宏任务的方式,让高优先级的宏任务更早执行,同时也设置了一个上限,防止下一阶段一直执行下去。还有一点需要特别注意,就是poll阶段:如果执行到poll阶段,发现poll队列为空,timers队列和check队列都没有要执行的任务,那么在这里阻塞并等待IO事件而不是空闲。这样设计也是因为服务器主要处理IO,阻塞在这里可以更早的响应IO。完整的Node.jsEventLoop是这样的:对比浏览器的EventLoop:两种JS运行环境下EventLoop的整体设计思路是相似的,只是Node.jsEventLoop做的更多是宏任务和微任务.也很容易理解,它有更细粒度的划分。毕竟Node.js的环境和浏览器是不一样的,更重要的是对服务器的性能要求会更高。总结JavaScript最早是用来编写网页交互逻辑的。为了避免多线程同时修改DOM的同步问题,设计成单线程。为了解决单线程的阻塞问题,增加了一层调度逻辑,即Loop循环和Task队列,将阻塞逻辑放在其他线程上运行,从而支持异步。然后,为了支持高优先级的任务调度,引入了微任务队列,也就是浏览器的EventLoop机制:每次执行一个macrotask,然后执行所有的microtask。Node.js也是一个JS运行环境。如果要支持异步,还需要使用EventLoop,但是服务器环境比较复杂,对性能要求也比较高。因此,Node.js对宏任务和微任务做了更细粒度的优先级划分。:Node.js中有五种宏任务,分别是Timers、Pending、Poll、Check、Close。microtasks也分为两类,分别是process.nextTickmicrotasks和othermicrotasks。Node.js的EventLoop流程是在当前阶段执行一定数量的宏任务(其余的在下一个循环执行),然后执行所有的微任务。总共有6个定时器,Pending、Idle/Prepare、Poll、Check和Close。阶段。Idle/Prepare阶段由Node.js内部使用,所以不用担心。应特别注意Poll阶段。如果poll队列为空,timers和check队列也为空,就会阻塞在这里等待IO,直到timers和check队列有回调后才继续循环。EventLoop是JS设计的一套支持异步和任务优先级的调度逻辑。它针对浏览器和Node.js等不同环境有不同的设计(主要是任务优先级的粒度不同)。Node.js所面临的环境更加复杂,对性能的要求也更高,因此EventLoop的设计也更加复杂。