1.同步任务与异步任务
(1)同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务;
(2)异步任务:不进入主线程而是进入任务队列的任务,只有等主线程的任务执行完毕后,任务队列开始通知主线程,请求将异步任务进入到主线程执行;
2.浏览器环境与node环境的事件循环机制
(1)浏览器环境:在HTML5中定义的规范;
js执行为单线程(不考虑web worker),所有代码皆在执行线程调用栈完成执行;当执行线程任务清空后才会去轮询取任务队列中任务。
- 任务队列
浏览器对不同的异步操作,将其添加到任务队列的时机也不同—由浏览器内核的webcore来执行,其包含3种webAPI:- DOM Binding:处理DOM绑定事件,若绑定事件触发时,回调函数立即被webcore添加到任务队列中;
- network:处理ajax请求,在网络请求返回时,才将对应的回调函数添加到队列中;
- timer:对setTimeout等计时器进行延时处理,当时间到达时才会将回调函数添加到任务队列中;
- 异步任务类别及执行顺序
- macrotask(宏任务—task):script中代码、setTimeout、setInterval、I/O、UI render。
-
microtask(微任务): promise、Object.observe、MutationObserver。
- 具体过程
(1)执行完主执行线程中的任务(初始执行线程中没有代码,每一个script标签中的代码是一个独立的macrotask)。
(2)取出Microtask Queue中任务执行直到清空(若microtask一直被添加,则会继续执行microtask,卡死macrotask)。
(3)取出Macrotask Queue中一个任务执行。
(4)取出Microtask Queue中任务执行直到清空。
(5)重复(3)和(4)
- 具体过程
(2)node环境:由libuv库实现;
node基于事件循环实现非阻塞和事件驱动,其事件循环按阶段执行;
-
阶段详情
(1)timers(定时器阶段):处理setTimeout()和setInterval()设定的回调函数队列;一个timer事件指定一个下限时间而不是准确的时间,在达到这个下限时间后+主线程空闲时,执行该事件对应的回调函数,从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers(poll阶段会控制是否进入下个timers阶段);
(2)I/O callbacks阶段:执行一些系统操作的回调(比如网络通信的错误回调);
(3)idle、prepare:仅供libuv内部调用;
(4)poll(轮询阶段): 等待还未返回的I/O事件,任何异步方法(除timers、setImmediate、close外)完成时,都会将其加到poll queue里,并立即执行;
-
主要功能:
(i) 处理poll队列里的事件;
(ii)执行下限时间已经达到的timers的回调(进入下一个事件循环) ; 当事件循环进入poll阶段:
(i)poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
(ii)poll队列为空的时候,这里有两种情况。
1)如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。
2)如果代码没有被设定setImmediate()设定回调:
* 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列(进入下一个事件循环阶段了)。
* 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。
(5)check阶段:执行setImmediate()设定的回调;
* setImmediate()实际上是一个特殊的timer,跑在事件循环中的一个独立的阶段;它使用libuv的API来设定在:
* poll阶段结束后立即执行回调;
* poll阶段空闲时,不让阻塞在poll阶段直接跳到check阶段执行回调。(6)close callbacks阶段:如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。
-
-
任务队列类型
原生的libuv事件循环处理的队列有4种主要类型:
(1)Timers Queue;
(2)I/O Queue;
(3)Check Queue;
(4)Close Queue;
中间队列有2种:
(1)Next tick队列:process.nextTick()
(2)Other Microtasks:包括其他 microtask,如 resolved promise回调;
** Next tick队列比Other Microtasks队列具有更高的优先级**;不过,它们都在事件循环的两个阶段之间进行处理,也就是在结束一个阶段后libuv通信回传到上层;注:NodeJS中不同类型的事件在自己的队列中排队;中间队列是只要一个阶段完成,事件循环就会检查这两个中间队列是否有可执行的任务,若有则立即处理它们直到为空,一旦为空,事件循环将继续到下一个阶段。
- 具体过程
(1)清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
(2)清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
(3)清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
(4)清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
(5)进入下轮循环(tick);
- 具体过程
4.代码
function sleep(time) {
let startTime = new Date()
while (new Date() - startTime < time) {}
console.log('1s over')
}
setTimeout(() => {
console.log('setTimeout - 1')
setTimeout(() => {
console.log('setTimeout - 1 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then - then')
})
})
sleep(1000)
})
setTimeout(() => {
console.log('setTimeout - 2')
setTimeout(() => {
console.log('setTimeout - 2 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then - then')
})
})
sleep(1000)
})
6.Node的异步I/O模型
(1)基本要素:事件循环、观察者、请求对象、IO线程池;
-
事件循环:典型的生产者/消费者模型;
- 事件的产生:网络请求、文件IO等操作;
- 事件的消费:主线程空闲时从观察者那儿取出事件并处理其回调;
-
观察者:在每个Tick的过程中,通过观察者判断是否有事件需要处理;
小剧场: * 主线程:饭馆的厨房; * 观察者:收银台的小妹; * 事件及回调函数:客人的点单; * 剧情:厨房一轮一轮炒菜,但是具体要炒什么菜取决于收银台收到的客人的下单。
-
请求对象: 从js发起回调到内核执行完IO操作的过渡过程中的中间产物;
以fs.open(path,flags,[mode],callback)打开某个文件为例:从js调用Node的核心模块->核心模块调用C++内建模块->内建模块调用libuv进行系统调用:uv_fs_open(): (1)创建一个FSReqWrap请求对象:封装js层传入的参数和open()方法,将回调函数设置到该对象的oncomplete_sym属性上; (2)将这个请求对象推入线程池中等待执行:当线程池有可用线程时,调用相应的底层函数:fs_open(); js调用完后立即返回,js线程可以继续执行当前任务的后续操作,当前的I/O操作在线程池中等待操作,不影响js线程。
- 执行回调:以windows平台为例
- 线程池中的I/O操作调用完后,会将获取的结果存储到req->result属性上,调用PostQueuedCompletionStatus()向IOCP(IO完成端口)提交执行状态,告知当前对象操作已经完成,并将线程归还线程池;
- I/O观察者在每次Tick的执行中,调用GetQueuedCompletionStatus()检查线程池是否有执行完的请求,若存在,将请求对象加入到I/O观察者队列中,然后将其当作事件处理:取出请求对象的result属性做参数,取出oncomplete_sym属性做方法,然后调用执行。
(2)基本流程
- 第一阶段:组装好对象 -> 送入IO线程池等待执行;
-
第二阶段:回调通知;
[参考文献]
1.NodeJS事件循环(英文版/中文版)
2.node中的Event模块
3.浏览器和Node不同的事件循环(Event Loop)
4.《深入浅出nodejs》