JavaScript引擎又称为JavaScript解释器,是JavaScript解释为机器码的工具,分别运行在浏览器和Node中。而根据上下文的不一样,Event loop也有不一样的实现:其中Node使用了libuv库来实现Event loop; 而在浏览器中,html规范定义了Event loop,具体的实现则交给不一样的厂商去完成。javascript
根据2017年新版的HTML规范HTML Standard,浏览器包含2类事件循环:browsing contexts 和 web workers。 html
browsing contexts中有一个或多个Task Queue,即MacroTask Queue,仅有一个Job Queue,即MicroTask Queue。html5
macrotask queue(宏任务,不妨称为A
)java
microtask queue(微任务,不妨称为I
)node
这两个任务队列执行顺序:git
A
中的task,执行之。I
顺序执行完,再取A
中的下一个任务。为何promise.then的回调比setTimeout先执行
代码开始执行时,全部这些代码在A
中,造成一个执行栈(execution context stack),取出来执行之。
遇到setTimeout,则加到A
中,遇到promise.then,则加到I
中。
等整个执行栈执行完,取I
中的任务。github
(function test() { setTimeout(function() {console.log(4)}, 0); new Promise(function executor(resolve) { console.log(1); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); })() // 1 // 2 // 3 // 5 // 4
//浏览器渲染步骤:Structure(构建 DOM) ->Layout(排版)->Paint(绘制) //新的异步任务将在下一次被执行,所以就不会存在阻塞。 button.addEventListener('click', () => { setTimeout(fn, 0) })
V8源码
https://github.com/v8/v8/blob...
https://github.com/v8/v8/blob...web
而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。c#
node新加了一个微任务process.nextTick
和一个宏任务setImmediate
.segmentfault
在当前"执行栈"的尾部(下一次Event Loop以前)触发回调函数。也就是说,它指定的任务老是发生在全部异步任务以前。
process.nextTick(function A() { console.log(1); process.nextTick(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0) // 1 // 2 // TIMEOUT FIRED
setImmediate
方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务老是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。
setImmediate(function A() { console.log(1); setImmediate(function B(){console.log(2);}); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); //不肯定
递归的调用process.nextTick()会致使I/O starving,官方推荐使用setImmediate()
process.nextTick(function foo() { process.nextTick(foo); }); //FATAL ERROR: invalid table size Allocation failed - JavaScript heap out of memory
process.nextTick也会放入microtask quque,为何优先级比promise.then高呢
在Node中,_tickCallback在每一次执行完TaskQueue中的一个任务后被调用,而这个_tickCallback中实质上干了两件事:
node.js的特色是事件驱动,非阻塞单线程。当应用程序须要I/O操做的时候,线程并不会阻塞,而是把I/O操做交给底层库(LIBUV)。此时node线程会去处理其余任务,当底层库处理完I/O操做后,会将主动权交还给Node线程,因此Event Loop的用处是调度线程,例如:当底层库处理I/O操做后调度Node线程处理后续工做,因此虽然node是单线程,可是底层库处理操做依然是多线程。
根据Node.js官方介绍,每次事件循环都包含了6个阶段,对应到 libuv 源码中的实现,以下图所示
timers :这个阶段执行timer(setTimeout、setInterval)的回调
I/O callbacks:执行一些系统调用错误,好比网络通讯的错误回调
idle, prepare :仅node内部使用
poll :获取新的I/O事件, 适当的条件下node将阻塞在这里
check :执行 setImmediate() 的回调
close callbacks :执行 socket 的 close 事件回调
timers 是事件循环的第一个阶段,Node 会去检查有无已过时的timer,若是有则把它的回调压入timer的任务队列中等待执行,事实上,Node 并不能保证timer在预设时间到了就会当即执行,由于Node对timer的过时检查不必定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。可是把它们放到一个I/O回调里面,就必定是 setImmediate() 先执行,由于poll阶段后面就是check阶段。
这个阶段主要执行一些系统操做带来的回调函数,如 TCP 错误,若是 TCP 尝试连接时出现 ECONNREFUSED 错误 ,一些 *nix 会把这个错误报告给 Node.js。而这个错误报告会先进入队列中,而后在 I/O callbacks 阶段执行。
poll 阶段主要有2个功能:
even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种状况:
注意一个细节,没有setImmediate()会致使event loop阻塞在poll阶段,这样以前设置的timer岂不是执行不了了?因此咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,若是timer队列非空,event loop就开始下一轮事件循环,即从新进入到timer阶段。
setImmediate()的回调会被加入check队列中, 从event loop的阶段图能够知道,check阶段的执行顺序在poll阶段以后。
忽然结束的事件的回调函数会在这里触发,若是 socket.destroy(),那么 close 会被触发在这个阶段,也有可能经过 process.nextTick() 来触发。
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) /*浏览器中 timer1 promise1 timer2 promise2 */ /*node中 timer1 timer2 promise1 promise2 */
const fs = require('fs') fs.readFile('test.txt', () => { console.log('readFile') setTimeout(() => { console.log('timeout') }, 0) setImmediate(() => { console.log('immediate') }) }) /* readFile immediate timeout */
更多示例
libuv源码
https://github.com/libuv/libu...
HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,若是低于这个值,就会自动增长。在此以前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变更(尤为是涉及页面从新渲染的部分),一般不会当即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()
客户端可能实现了一个包含鼠标键盘事件的任务队列,还有其余的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它。这样就能保证流畅的交互性,并且别的任务也能执行到了。可是,同一个任务队列中的任务必须按先进先出的顺序执行。
用户点击与button.click()的区别:
用户点击:依次执行listener。浏览器并不实现知道有几个 listener,所以它发现一个执行一个,执行完了再看后面还有没有。
click:同步执行listener。 click方法会先采集有哪些 listener,再依次触发。
示例详情
参考资料
Promise的队列与setTimeout的队列有何关联?
浏览器的 Event Loop
Event Loops
深刻理解js事件循环机制(Node.js篇)
JavaScript 运行机制详解:再谈Event Loop
Node.js 事件循环,定时器和 process.nextTick()