一、Event Loop是什么?
概念:
Event Loop即事件循环,是一个执行模型,主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环,
目的:
是浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
二、为什么会有Event Loop?
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言,单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务,也就是说,同一个时间只能做一件事,所以js中有事件循环,是因为js是单线程的原因。
三、Event Loop模型有哪些?
在不同的地方有不同的实现,浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
**浏览器:**Event Loop是在html5的规范中明确定义。
Node-JS:Event Loop是基于libuv实现的。可以参考Node以及libuv的官方文档。
libuv已经对Event Loop做出了实现,而HTML5规范中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。
四、任务队列(task queue)
是一种先进先出的一种数据结构。
由于js是单线程的,只有当上一个任务完成之后才会继续完成下一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
**同步任务:**在调用栈中按照顺序等待主线程依次执行(在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务);
**异步任务:**不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
宏任务(macrotask )和微任务(microtask ),表示异步任务的两种分类
-
setTimeout
-
setInterval
-
setImmediate (Node独有)
-
requestAnimationFrame (浏览器独有)
-
I/O
-
UI rendering (浏览器独有)
-
包括整体代码 script
微任务:,可以理解是在当前 task 执行结束后立即执行的任务 ,包括以下内容:
-
process.nextTick (Node独有)
-
Promise
-
Object.observe
-
MutationObserver
五、浏览器的Event Loop
注意:
- 宏任务macrotask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
- 当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个事件。同一次事件循环中, 微任务永远在宏任务之前执行。
- 在执行微队列microtask queue中任务的时候,如果又产生了microtask,那么会继续添加到队列的末尾,也会在这个周期执行,直到microtask queue为空停止。
先看一个简单的示例:
setTimeout(()=>{
console.log("setTimeout1");
Promise.resolve().then(data => {
console.log(222);
});
});
setTimeout(()=>{
console.log("setTimeout2");
});
Promise.resolve().then(data=>{
console.log(111);
});
思考一下, 运行结果是什么?
运行结果为:
111
setTimeout1
222
setTimeout2
再思考一下下面代码的执行顺序:
console.log('script start');
setTimeout(function () {
console.log('setTimeout---0');
}, 0);
setTimeout(function () {
console.log('setTimeout---200');
setTimeout(function () {
console.log('inner-setTimeout---0');
});
Promise.resolve().then(function () {
console.log('promise5');
});
}, 200);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
Promise.resolve().then(function () {
console.log('promise3');
});
console.log('script end');
思考一下, 运行结果是什么?
运行结果为:
script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
五、NodeJS的Event Loop
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。
NodeJS的Event Loop,执行宏队列的回调任务有6个阶段,如下图:
从上图模型中,我们可以大致分析出node中事件循环顺序:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...
阶段功能:
- timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()。
- I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
- idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check: setImmediate()的回调会在这个阶段执行。
- close callbacks: 例如socket.on('close', ...)这种close事件的回调。
NodeJS的Event Loop过程:
-
执行全局Script的同步代码
-
执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
-
开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务;
六、NodeJS与浏览器的Event Loop主要差异
浏览器:microtask的任务队列是每个macrotask执行之后执行。
nodejs:microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。