JS是一门单线程的语言,若是没有异步操做的话,一个很耗时的操做,就能够堵塞整个进程。而出现异步操做以后,就会有数据通讯之间的问题,而event loop很好的解决了这个问题。html
什么是Event loop?这是咱们第一个须要知道的问题。 在html官方标准中是这么介绍的。node
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.web
为了协调事件,用户交互,脚本运行,页面渲染,网络请求等,用户代理必须使用本节描述event loop。有两种event loop,一种是browsing contexts,另外一种是workers.ajax
在标准文档中能够看到两种task,一种就叫task,还有一种叫Microtask。编程
1、taskapi
An event loop has one or more task queues. A task queue is an ordered list of taskspromise
规范中指出一个事件循环有一个或者多个任务,任务被有序的排列在队列中。这里咱们列举几个典型的任务源:浏览器
2、microtaskbash
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.网络
规范中也指出,每个event loop只有一个微任务队列,微任务一般只排列在微任务队列上,而不是任务队列。这里有两种微任务:回调微任务和复合微任务。举几个典型的微任务:
3、event loop运行机制
在写这个以前,先写几条总结出来的规律:
用伪代码表示为:
一个任务,清空微任务栈,一个任务,清空微任务栈,...
关于整个运行过程,能够参见规范第8章
4、example
// 简称set1
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
// 简称set2
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
// 简称set3
setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('start')
复制代码
循环一
一、将脚本任务放入到task队列。
二、从task中取出一个任务运行,运行的结果是将set1和set2放入到task中,将promise.then放入到microtask中,输出start。
三、检查microtask checkpoint,看microtask队列中是否有任务。
四、运行microtask中全部的任务,输出promise3。
五、清空microtask队列以后,进入下一个循环。
循环二
一、从task中在取出一个set1任务,运行的结果是输出timer1,将promise.then放入到microtask队列中。
二、检查microtask checkpoint,看microtask队列中是否有任务。
三、运行microtask中全部的任务,输出promise1。
四、清空microtask队列以后,进入下一个循环。
循环三
一、从task中在取出一个set2任务,运行的结果是输出timer2,将promise.then放入到microtask队列中,将set3放入到task队列中。
二、检查microtask checkpoint,看microtask队列中是否有任务。
三、运行microtask中全部的任务,输出promise2。
四、清空microtask队列以后,进入下一个循环。
循环四
一、从task中在取出一个set3任务,运行的结果是输出timer3
二、检查microtask checkpoint,看microtask队列中没有任务,进入下一个循环。
循环五
检测task队列和microtask队列都为空,WorkerGlobalScope对象中closing标志位为true,销毁event loop。
start
promise3
timer1
promise1
timer2
promise2
timer3
复制代码
咱们先来看一下node的架构。
node的异步是经过底层的libuv来实现的。
1、libuv是什么
libuv enforces an asynchronous, event-driven style of programming. Its core job is to provide an event loop and callback based notifications of I/O and other activities. libuv offers core utilities like timers, non-blocking networking support, asynchronous file system access, child processes and more.
libuv使用异步和事件驱动的编程风格。它的核心工做是提供一个event-loop,还有基于I/O和其它事件通知的回调函数。libuv还提供了一些核心工具,例如定时器,非阻塞的网络支持,异步文件系统访问,子进程等。
2、libuv中的event loop
在node的官方doc中,将El分红了六个阶段,咱们能够看一下下面的图:
当node开始运行的时候,它会初始化一个event loop,而每一个event loop都包含如下六个阶段:
每个阶段都有一个回调的FIFO队列,当EL运行到一个指定阶段的时候,node将会执行这个队列,当队列中全部的回调都执行完或者执行的回调数上限的时候,EL会跳到下一个阶段。以上全部阶段不包含process.nextTick()。
整个的EL运行过程源码注释版:
//deps/uv/src/unix/core.c
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
int timeout;
int r;
int ran_pending;
//uv__loop_alive返回的是event loop中是否还有待处理的handle或者request
//以及closing_handles是否为NULL,若是均没有,则返回0
r = uv__loop_alive(loop);
//更新当前event loop的时间戳,单位是ms
if (!r)
uv__update_time(loop);
while (r != 0 && loop->stop_flag == 0) {
//使用Linux下的高精度Timer hrtime更新loop->time,即event loop的时间戳
uv__update_time(loop);
//执行判断当前loop->time下有无到期的Timer,显然在同一个loop里面timer拥有最高的优先级
uv__run_timers(loop);
//判断当前的pending_queue是否有事件待处理,而且一次将&loop->pending_queue中的uv__io_t对应的cb所有拿出来执行
ran_pending = uv__run_pending(loop);
//实如今loop-watcher.c文件中,一次将&loop->idle_handles中的idle_cd所有执行完毕(若是存在的话)
uv__run_idle(loop);
//实如今loop-watcher.c文件中,一次将&loop->prepare_handles中的prepare_cb所有执行完毕(若是存在的话)
uv__run_prepare(loop);
timeout = 0;
//若是是UV_RUN_ONCE的模式,而且pending_queue队列为空,或者采用UV_RUN_DEFAULT(在一个loop中处理全部事件),则将timeout参数置为
//最近的一个定时器的超时时间,防止在uv_io_poll中阻塞住没法进入超时的timer中
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
//进入I/O处理的函数(重点分析的部分),此处挂载timeout是为了防止在uv_io_poll中陷入阻塞没法执行timers;而且对于mode为
//UV_RUN_NOWAIT类型的uv_run执行,timeout为0能够保证其当即跳出uv__io_poll,达到了非阻塞调用的效果
uv__io_poll(loop, timeout);
//实如今loop-watcher.c文件中,一次将&loop->check_handles中的check_cb所有执行完毕(若是存在的话)
uv__run_check(loop);
//执行结束时的资源释放,loop->closing_handles指针指向NULL
uv__run_closing_handles(loop);
if (mode == UV_RUN_ONCE) {
//若是是UV_RUN_ONCE模式,继续更新当前event loop的时间戳
uv__update_time(loop);
//执行timers,判断是否有已经到期的timer
uv__run_timers(loop);
}
r = uv__loop_alive(loop);
//在UV_RUN_ONCE和UV_RUN_NOWAIT模式中,跳出当前的循环
if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
break;
}
//标记当前的stop_flag为0,表示当前的loop执行完毕
if (loop->stop_flag != 0)
loop->stop_flag = 0;
//返回r的值
return r;
}
复制代码
能够结合上面的六个过程看一下。
3、poll阶段
在进入poll阶段以前,会先对timeout进行处理
timeout = 0;
if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
timeout = uv_backend_timeout(loop);
uv__io_poll(loop, timeout);
复制代码
timeout做为uv__io_poll
的第二个参数,当timeout等于0的时候会跳过poll阶段。
咱们能够看一下uv_backend_timeout
的源码。
int uv_backend_timeout(const uv_loop_t* loop) {
if (loop->stop_flag != 0)
return 0;
if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
return 0;
if (!QUEUE_EMPTY(&loop->idle_handles))
return 0;
if (!QUEUE_EMPTY(&loop->pending_queue))
return 0;
if (loop->closing_handles)
return 0;
return uv__next_timeout(loop);
}
复制代码
以上五种状况(退出事件循环,没有任何异步任务,idle_handles和pending_queue不为空,循环进入到closing_handles)返回的timeout都为0。
uv__next_timeout
源码
int uv__next_timeout(const uv_loop_t* loop) {
const struct heap_node* heap_node;
const uv_timer_t* handle;
uint64_t diff;
heap_node = heap_min((const struct heap*) &loop->timer_heap);
if (heap_node == NULL)
return -1; /* block indefinitely */
handle = container_of(heap_node, uv_timer_t, heap_node);
if (handle->timeout <= loop->time)
return 0;
//这句代码给出了关键性的指导
diff = handle->timeout - loop->time;
//不能大于最大的INT_MAX
if (diff > INT_MAX)
diff = INT_MAX;
return diff;
}
复制代码
diff表明的是,距离最近的一个异步回调的时间。最大是32767微秒。而后将diff做为timeout的值,传递给poll阶段。
poll阶段主要有两个功能: 一、计算poll阶段堵塞和轮询还有多长时间。 二、处理poll阶段中的事件。
当EL进入到poll阶段的时候,若是代码中没有设定的timers,那么会发生如下两种状况:
若是poll队列不是空的,将执行poll阶段里面的cb,直到cb为空,或者执行的cb达到上限。
若是poll为空的状况,又会有两种状况发生:
一旦poll阶段是空的,EL会检查是否有到期的timers,若是有一个或者多个已经到达,那么会直接跳到timers阶段执行timers的回调。
用一张图表示:
4、setImmediate() vs setTimeout()
二者的用法是类似的,而setImmediate进入的是check阶段,而setTimeout进入的是timer的阶段。
而在node的docs中举了个例子,以下:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
复制代码
而在屡次执行中,二者的触发顺序不必定相同:
$ node timeout_vs_immediate.js
timeout
immediate
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
而将其放在i/o中执行,二者的顺序是固定的:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
复制代码
输出的结果:
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout
复制代码
在node中,计时器的时间是精确到毫秒级别的,因此setTimeout(cb, 0) === setTimeout(cb, 1)。 EL初始化是须要耗时的,可是hrtime这个值精确到纳秒级别,因此整个脚本运行会发生如下两种状况:
一、loop准备时间超过1ms,那么loop->time >=1,就会发生
uv_run_timers
。 二、loop准备时间小于1ms,那么loop->time<1,uv_run_timers
不生效,就会直接到后面的check阶段去。
而若是有fs的状况下,直接走的是uv__io_poll
,触发回调以后,直接走check,在走timer阶段。
5、process.nextTick()
process.nextTick()在node中不参与任何阶段,可是每当切换阶段的时候,须要清空process.nextTick()队列中的回调。
看一个例子:
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(()=>{
console.log('nextTick3');
})
});
process.nextTick(()=>{
console.log('nextTick1');
})
process.nextTick(()=>{
console.log('nextTick2');
})
});
复制代码
输出结果:
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
复制代码
整个循环过程: 循环一:
一、进来的时候,直接进入poll阶段,执行回调。
二、挂载setTimeout,挂载setImmediate,将process.nextTick推动nextTick队列中
三、先执行nextTick队列,输出nextTick1和nextTick2。
四、进入check阶段,执行setImmediate回调,输出setImmediate。
五、在执行nextTick队列,输出nextTick3。
循环二:
一、进入timer阶段,有到期的定时器,输出setTimeout。