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

「进击前端工程师」Node.js事件循环

时间:2023-04-04 01:04:09 Node.js

欣赏:??????口味:法式鹅肝烹饪时间:20min本文已收录于Githubgithub.com/Geekhyt,感谢Star。事件循环事件循环的执行顺序从图中可以看出,每个事件循环包括上图中的6个阶段,接下来我们就一一解读。timersTimer定时器分为两种类型:ImmediateTimeout定时器在超时后的下一个校验阶段执行(delay参数默认值为1ms)。Timeout定时器有两种类型:IntervalTimeout阶段会执行setTimeout()和setInterval()poll阶段设置的回调定时器的执行由setTimeout()和setInterval()控制,浏览器中的API是相同的。它们的实现原理类似于异步I/O,但不需要I/O线程池的参与。两个定时器创建完成后,会被插入到定时器观察者内部的一颗红黑树中。每执行一次Tick,就会从红黑树中取出定时器对象,检查是否超过了计时时间,如果超过了时间限制就会执行它们的回调。注意:计时器的一个问题是它们不是绝对准确的(在公差范围内)。一旦某个任务在某个事件循环中占用的时间较多,再次轮到定时器执行时,时间就会受到影响。无IO处理情况通过执行上面的代码我们可以发现输出的结果是不确定的。因为setTimeout(fn,0)有几毫秒的不确定性,不能保证一定会进入timers阶段,timer能马上执行handler。有IO处理情况varfs=require('fs');fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0);setImmediate(()=>{console.log('immediate');});})//immediate//timeout此时setImmediate先于setTimeout执行,因为poll阶段执行完成后进入check阶段,而timers阶段在下一个eventloop阶段。PendingcallbacksPendingcallbacks执行大部分回调,除了close,times,setImmediate()设置的回调idle,perpare只是内部使用poll轮询获取新的I/O事件,在合适的条件下,Node.js会主要这里阻塞这个阶段的任务是当延迟时间到达时执行timers定时器的回调,处理poll队列中的事件。当事件循环进入轮询阶段,定时器没有被调用时,会发生以下两件事:1.如果轮询队列不为空,事件循环会遍历回调队列同步执行。2.如果poll队列为空,有两种情况:如果是setImmediate()回调调用,事件循环结束poll阶段,进入check阶段。如果没有被setImmediate()回调调用,事件循环将阻塞并等待回调被添加到轮询队列中执行。一旦轮询队列为空,事件循环将检查计时器是否已达到其延迟时间。如果一个或多个定时器已经达到它们的延迟时间,事件循环将回滚到定时器阶段并执行它们的回调。checkdetectionsetImmediate()设置的回调会在这个阶段执行,和上面poll阶段的第二种情况一样。如果poll队列为空,被setImmediate()回调调用,事件循环会直接进入check阶段。closecallbacks的回调函数socket.on('close',callback)会在这个阶段执行libuv。libuv为Node.js提供了完整的事件循环功能。如上图所示,Windows下,事件循环基于IOCP创建,Linux下通过epoll实现,FreeBSD下通过kqueue实现,Solaris下通过Eventports实现。让我们仔细看看上图。NetworkI/O、文件I/O、DNS的实现方式是分开的,因为它们本质上是由两套机制实现的。一会儿我们通过源码一窥它们的本质。本质上,当我们编写JavaScript代码调用Node的核心模块时,核心模块会调用C++内置模块,内置模块会通过libuv进行系统调用。libuv主要解决的问题在现实世界中,在所有不同类型的操作系统平台下都支持不同类型的I/O是非常困难的。那么为了支持跨平台I/O,更好的管理整个过程,libuv被抽象出来。简单的说,libuv抽象了一层API,可以帮助你调用各种平台和机器上的各种系统功能,包括操作文件、监听socket等,你不需要了解它们的具体实现。核心源码解读核心函数uv_run源码intuv_run(uv_loop_t*loop,uv_run_modemode){inttimeout;诠释;intran_pending;//检查循环中是否有异步任务,没有则结束。r=uv__loop_alive(loop);如果(!r)uv__update_time(循环);//事件循环while(r!=0&&loop->stop_flag==0){//更新事件阶段uv__update_time(loop);//处理定时器回调uv__run_timers(loop);//处理异步任务回调ran_pending=uv__run_pending(loop);//供内部使用uv__run_idle(loop);uv__run_prepare(循环);//uv_backend_timeout计算出来后传给uv__io_poll//如果timeout=0,那么uv__io_poll会直接跳过timeout=0;如果((mode==UV_RUN_ONCE&&!ran_pending||mode==UV_RUN_DEFAULT))timeout=uv_backend_timeout(loop);uv__io_poll(循环,超时);//检查阶段uv__run_check(loop);//关闭文件描述符等操作uv__run_closing_handles(loop);//检查循环中是否有异步任务,没有则结束。r=uv__loop_alive(loop);如果(模式==UV_RUN_ONCE||模式==UV_RUN_NOWAIT)中断;}returnr;}事件循环的真面目是一会儿。上面提到了网络I/O、文件I/O、DNS等都是通过两种机制实现的。首先,让我们看看网络I/O。它的最终调用会归于uv__io_start函数,该函数会将需要执行的I/O事件和回调放入watcher队列,uv__io_poll阶段会从watcher队列中取出事件。调用系统的接口并执行。(uv__io_poll部分代码太长,有兴趣的可以自行查看)uv__io_startvoiduv__io_start(uv_loop_t*loop,uv__io_t*w,unsignedintevents){assert(0==(events&~(POLLIN|POLLOUT|UV__POLLRDHUP|UV__POLLPRI)));断言(0!=事件);断言(w->fd>=0);断言(w->fdpevents|=事件;maybe_resize(loop,w->fd+1);如果(w->events==w->pevents)返回;if(QUEUE_EMPTY(&w->watcher_queue))QUEUE_INSERT_TAIL(&loop->watcher_queue,&w->watcher_queue);if(loop->watchers[w->fd]==NULL){loop->watchers[w->fd]=w;循环->nfds++;}}如上图,就是libuv中NetworkI/O主线的实现过程。另一条主线是FsI/O和DNS操作会调用uv__work_sumit函数,也就是线程池初始化uv_queue_work最后调用的函数。voiduv__work_submit(uv_loop_t*loop,structuv__work*w,enumuv__work_kindkind,void(*work)(structuv__work*w),void(*done)(structuv__work*w,intstatus)){uv_once(&once,init_once)复制代码;w->循环=循环;w->工作=工作;w->完成=完成;post(&w->wq,kind);}intuv_queue_work(uv_loop_t*loop,uv_work_t*req,uv_work_cbwork_cb,uv_after_work_cbafter_work_cb){if(work_cb==NULL)返回UV_EINVAL;uv__req_init(loop,req,UV_WORK);请求->循环=循环;req->work_cb=work_cb;req->after_work_cb=after_work_cb;uv__work_submit(loop,&req->work_req,UV__WORK_CPU,uv__queue_work,uv__queue_done);return0;}Node.js中的事件队列Node.js中有多个队列,不同类型的事件在各自的队列中排队。一个阶段结束后,在进入下一阶段之前,事件循环会处理中间的中间队列。原生libuv事件循环中主要有四种队列类型:expiredtimers和intervalqueuesIOeventqueuesImmediatesqueuesclosehandlersqueue另外,Node.js有两种中间队列NextTicksqueueOtherMicrotasksqueueNodeEventLoop的区别.js与浏览器的JavaScript事件循环我们可以回顾一下浏览器中的JavaScript事件循环,请移步我的其他系列专栏《进击的前端工程师》系列-浏览器中的JavaScript事件循环回来后,先说结论:在浏览器,microtask的任务队列在每个macrotask执行完后执行。在Node.js中,microtask会在eventloop的各个stage之间执行,也就是一个stage执行完之后,microtask队列中的task就会被执行。(本文中的Macrotask在WHATWG中称为task,为了便于理解,Macrotask没有实际出处。)node相比浏览器多了两个异步操作:setImmediate(macrotask)和process.nextTick(microtask)。.setImmediate的回调函数在检查阶段执行。而process.nextTick会被视为一个microtask,所有的microtasks都会在每个stage之后执行。你可以理解为process.nextTick可以跳入队列,在下一阶段之前执行。process.nextTick跳队带来的危害process.nextTick的回调会导致事件循环无法进入下一阶段。即使I/O处理完成或定时器超时,也无法执行I/O处理。它会让其他事件处理程序饿死。为了防止这个问题,Node.js提供了一个process.maxTickDepth(默认为1000)。Node.js中的微任务nextTick')});//nextTick//那么我们可以看到nextTick执行的比then早。Node.jsv11改变的事件循环从Node.jsv11开始,事件循环的原理发生了变化。在同一阶段,只要执行了macrotask,microtask队列就会立即执行,这与浏览器的性能是一致的。有关详细信息,请参阅此pr。??看完三件事1.看到这里请点赞支持我。你的喜欢是我创作的动力。2.关注公众号前端食堂,你的前端食堂,记得按时吃饭!3.冬天了,多穿点衣服,别着凉~!

猜你喜欢