本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或从新修改使用,但需注明来源。署名 4.0 国际 (CC BY 4.0)前端
观感度:🌟🌟🌟🌟🌟node
口味:法式鹅肝linux
烹饪时间:20mingit
事件循环的执行顺序从图中能够看出,每次的事件循环都包含了上图中的6个阶段,接下来咱们来一一解读它们。github
计时器分为两类:web
Timeout计时器又有两种类型:浏览器
这个阶段会执行setTimeout()和setInterval()设定的回调
前端工程师
timers的执行是由poll阶段控制的
异步
setTimeout()和setInterval()和浏览器中的API是相同的。它们的实现原理与异步I/O比较相似,可是不须要I/O线程池的参与。socket
这两个定时器建立后会被插入到定时器观察者内部的一个红黑树中。每次Tick执行时,都会从红黑树中取出定时器对象,来检查它们是否超过定时时间,超过便执行它们的回调。
注意:定时器存在一个问题,就是它不是绝对精确的(在容忍范围内)。一旦某个事件循环中,有一个任务占用了较多的时间,那么再次轮到定时器执行时,时间就会受到影响。
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
复制代码
经过执行上面的代码咱们能够发现,输出结果是不肯定的。
由于setTimeout(fn, 0)具备几毫秒的不肯定性,没法保证进入timers阶段,定时器能当即执行处理程序。
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
})
// immediate
// timeout
复制代码
此时setImmediate优先于setTimeout执行,由于poll阶段执行完成后进入check阶段,而timers阶段则处于下一个事件循环阶段了。
执行大部分回调,除了close,times和setImmediate()设定的回调
仅供内部使用
获取新的I/O事件,在适当的条件下,Node.js会在这里阻塞
这个阶段的主要任务是执行到达delay时间的timers定时器的回调,而且处理poll队列里的事件。
当事件循环进入poll阶段,而且没有调用定时器时,将会发生如下两种状况:
1.若是poll队列不为空,事件循环将遍历同步执行它们的回调队列。
2.若是poll队列为空,又分为两种状况:
若是被setImmediate()回调调用,事件循环会结束poll阶段,进入到check阶段。
若是没有被setImmediate()回调调用,事件循环将阻塞并等待回调添加到poll队列中执行。
一旦poll队列为空,事件循环将查看计时器是否到达delay时间,若是一个或多个定时器已达到delay时间,事件循环将回滚到timers定时器阶段,执行它们的回调。
setImmediate()设定的回调会在这一阶段执行
如同上文poll阶段的第二种状况中,若是poll队列为空,而且被setImmediate()回调调用,事件循环将直接进入check阶段。
socket.on('close',callback)的回调会在这个阶段执行
libuv为Node.js提供了整个事件循环功能。
如上图所示,在Windows下,事件循环基于IOCP
建立,在linux下经过epoll
实现,FreeBSD下经过kqueue
实现,在Solaris下经过Event ports
实现。
咱们再细心的去看上图,Network I/O和file I/O、DNS等实现方式是被分隔开的,这是由于他们的本质是由两套机制来实现的。咱们一下子来经过源码窥探它们的本质。
实质上,当咱们写JavaScript代码去调用Node的核心模块时,核心模块会调用C++内建模块,内建模块经过libuv进行系统调用。
在现实世界中,在全部不一样类型的操做系统平台下,支持不一样类型的I/O是很是困难的。那么为了支持跨平台I/O的同时,能更好的管理整个流程,抽象出了libuv。
简单说,就是libuv抽象出一层API,能够帮助你调用各个平台和机器上各类系统特性,包括操做文件、监听socket等,而你不须要了解它们的具体实现。
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
// 检查loop中是否有异步任务,没有就结束。
r = uv__loop_alive(loop);
if (!r)
uv__update_time(loop);
// 事件循环while
while (r != 0 && loop->stop_flag == 0) {
// 更新事件阶段
uv__update_time(loop);
// 处理timer回调
uv__run_timers(loop);
// 处理异步任务回调
ran_pending = uv__run_pending(loop);
// 供内部使用
uv__run_idle(loop);
uv__run_prepare(loop);
// uv_backend_timeout计算完毕后,会传给uv__io_poll
// 若是timeout = 0,则uv__io_poll会直接跳过
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending || mode == UV_RUN_DEFAULT))
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
// check阶段
uv__run_check(loop);
// 关闭文件描述符等操做
uv__run_closing_handles(loop);
// 检查loop中是否有异步任务,没有就结束。
r = uv__loop_alive(loop);
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
return r;
}
复制代码
事件循环的真实面目是一个while。
上文说到Network I/O与file I/O、DNS等是由两套机制来实现的。
首先咱们来看Network I/O,它最后的调用都会归结到uv__io_start
这个函数,而该函数会将须要执行的I/O事件和回调放入watcher队列中,而uv__io_poll
阶段会从watcher队列中取出事件调用系统的接口并执行。
(uv__io_poll
部分的代码过长你们感兴趣可自行查看)
void uv__io_start(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
assert(0 == (events & ~(POLLIN | POLLOUT | UV__POLLRDHUP | UV__POLLPRI)));
assert(0 != events);
assert(w->fd >= 0);
assert(w->fd < INT_MAX);
w->pevents |= events;
maybe_resize(loop, w->fd + 1);
if (w->events == w->pevents)
return;
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;
loop->nfds++;
}
}
复制代码
如上所示就是咱们libuv中Network I/O这条主线实现过程。
而另一条主线是Fs I/O和DNS等操做则会调用uv__work_sumit
这个函数,这个函数是执行线程池初始化uv_queue_work
中最终调用的函数。
void uv__work_submit(uv_loop_t* loop, struct uv__work* w, enum uv__work_kind kind, void (*work)(struct uv__work* w), void (*done)(struct uv__work* w, int status)) {
uv_once(&once, init_once);
w->loop = loop;
w->work = work;
w->done = done;
post(&w->wq, kind);
}
复制代码
int uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb) {
if (work_cb == NULL)
return UV_EINVAL;
uv__req_init(loop, req, UV_WORK);
req->loop = loop;
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);
return 0;
}
复制代码
Node.js中有多个队列,不一样类型的事件在各自的队列中排队。在一个阶段结束后,进入下一个阶段以前,事件循环会在这中间处理中间队列。
原生的libuv事件循环中的队列主要又4种类型:
过时的定时器和间隔队列
IO事件队列
Immediates队列
close handlers队列
除此以外,Node.js还有两个中间队列
Next Ticks队列
Other Microtasks队列
咱们能够回顾下浏览器中JavaScript事件循环,请移步个人另外一篇系列专栏《进击的前端工程师》系列-浏览器中JavaScript的事件循环
回来后,先说结论:
在浏览器中,microtask的任务队列是每一个macrotask执行完以后执行。
在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。
(本文的Macrotask在WHATWG 中叫task。Macrotask为了便于理解,并无实际的出处。)
相比于浏览器,node多出了setImmediate(宏任务)
和process.nextTick(微任务)
这两种异步操做。
setImmediate
的回调函数被放在check
阶段执行。而process.nextTick
会被当作一种microtask
,每一个阶段结束后都会执行全部的microtask
,你能够理解为process.nextTick
能够插队,在下个阶段前执行。
process.nextTick的回调会致使事件循环没法进入到下一个阶段。I/O处理完成或者定时器过时后仍然没法执行。会让其余的事件处理程序处于饥饿状态,为了防止这个问题,Node.js提供了一个process.maxTickDepth
(默认为1000)。
Promise.resolve().then(function(){
console.log('then')
})
process.nextTick(function(){
console.log('nextTick')
});
// nextTick
// then
复制代码
咱们能够看到nextTick要早于then执行。
从Node.js v11开始,事件循环的原理发生了变化,在同一个阶段中只要执行了macrotask就会当即执行microtask队列,与浏览器表现一致。具体请参考这个pr。
1.看到这里了就点个赞支持下吧,你的点赞是我创做的动力。
2.关注公众号前端食堂
,你的前端食堂,记得按时吃饭!
3.入冬了,多穿衣服不要着凉~!