JavaScript引擎,又称JavaScript解释器,是一个将JavaScript解释成机器码的工具,分别运行在浏览器和Node中。根据上下文的不同,事件循环也有不同的实现:Node使用libuv库来实现事件循环;在浏览器中,html规范定义了事件循环,具体实现留给不同的厂商来完成。浏览器中的事件循环根据2017年新版HTML规范HTMLStandard,浏览器包含两种类型的事件循环:浏览上下文和网络工作者。浏览上下文中有一个或多个任务队列,即MacroTaskQueue,只有一个JobQueue,即MicroTaskQueue。macrotaskqueue(macrotask,不妨叫作A)setTimeoutsetIntervalsetImmediate(nodeunique)requestAnimationFrameI/OUIrenderingmicrotaskqueue(microtask,不妨叫作I)process.nextTick(nodeunique)PromisesObject.observe(废弃)MutationObserver这两个任务队列执行顺序:在A中取一个任务执行。依次执行所有I,然后接A中的下一个任务。为什么promise.then的回调会执行setTimeout之前的代码?当代码开始执行时,A中所有这些代码形成一个执行上下文栈(executioncontextstack),取出执行。遇到setTimeout就加到A,遇到promise.then就加到I,整个执行栈执行完毕后,就拿I里面的任务。(functiontest(){setTimeout(function(){console.log(4)},0);newPromise(functionexecutor(resolve){console.log(1);for(vari=0;i<10000;i++){i==9999&&resolve();}console.log(2);}).then(函数(){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...NodeJS中的EventLoop在Node.js中,microtask会在eventloop的各个stage之间执行,也就是执行完一个stage之后,microtask队列中的任务就会被执行。Node添加了一个微任务process.nextTick和一个宏任务setImmediate.process.nextTick来触发当前“执行堆栈”末尾的回调函数(在下一个事件循环之前)。也就是说,它指定的任务总是发生在所有异步任务之前。process.nextTick(functionA(){console.log(1);process.nextTick(functionB(){console.log(2);});});setTimeout(functiontimeout(){console.log('TIMEOUTFIRED');},0)//1//2//TIMEOUTFIREDsetImmediatesetImmediate方法在当前“任务队列”的末尾添加一个事件,即它指定的任务总是在下一个EventLoop中执行,这与setTimeout(fn,0)非常相似。setImmediate(functionA(){console.log(1);setImmediate(functionB(){console.log(2);});});setTimeout(functiontimeout(){console.log('TIMEOUTFIRED');},0);//不确定递归调用process.nextTick()会导致I/O饿死,官方推荐使用setImmediate()process.nextTick(functionfoo(){process.nextTick(foo);});//FATALERROR:invalidtablesizeAllocationfailed-JavaScriptheapoutofmemoryprocess.nextTick也会被放入microtaskquque,为什么优先级高于promise.then?在Node中,每次调用_tickCallback都会执行TaskQueue中的一个任务,而这个_tickCallback实际上做了两件事:执行完nextTickQueue中的所有任务(最大长度1e4,Node版本v6.9.1)执行完第一步后,_runMicrotasks函数为executed,microtask中的部分被执行(promise.then注册回调)所以很明显process.nextTick>promise.then”node.js的特点是事件驱动,非阻塞单线程。当应用程序需要I/O操作时,线程不会阻塞,而是将I/O操作交给底层库(LIBUV)。这个时候节点线程会去处理其他的任务。当底层库处理完I/O操作后,会把主动权交还给Node线程,所以EventLoop的目的就是调度线程,例如:当底层库处理完I/O操作时,Node线程是调度处理后续工作,所以虽然node是单线程的,但是底层的库处理操作仍然是多线程的。根据Node.js官方介绍,每个事件循环包括6个阶段,对应libuv源码中的实现,如下图timers:这个阶段执行timer(setTimeout,setInterval)I/O的回调callbacks:执行系统调用错误,如网络通信错误callbacksidle,prepare:仅供节点内部使用poll:获取新的I/O事件,节点在适当的条件下会阻塞在这里check:执行setImmediate()回调closecallbacks:执行sockettimers阶段的close事件回调timers是事件循环的第一阶段,Node会检查是否有过期的timer,如果有则将其回调推入timer的任务队列中执行,实际上Node是这样做的not不能保证定时器在预设时间一到就立即执行,因为Node对定时器的过期检查不一定靠谱,会受到机器上其他正在运行的程序的影响,或者主线程没有空闲当时。但是把它们放在I/O回调中,setImmediate()必须先执行,因为轮询阶段之后是检查阶段。I/O回调阶段这个阶段主要执行一些系统操作带来的回调函数,比如TCP错误。如果在TCP尝试连接时出现ECONNREFUSED错误,某些*nix将向Node.js报告此错误。而这个报错会先进入队列,然后在I/O回调阶段执行。轮询阶段轮询阶段有两个主要功能:处理轮询队列中的事件。当有定时器超时时,执行其回调函数even循环会同步执行poll队列中的回调,直到队列为空或执行的回调达到系统上限(上限未知),然后evenloop会检查是否有presetsetImmediate(),分两种情况:如果有presetsetImmediate(),eventloop会结束poll阶段进入check阶段,如果没有preset则执行check阶段任务队列中的setImmediate(),事件循环会阻塞在这个阶段等待,注意一个细节。如果没有setImmediate(),事件循环会阻塞在poll阶段,那么之前设置的定时器就不能执行了?所以,在轮询阶段,事件循环会有一个检查机制来检查定时器队列是否为空。如果定时器队列不为空,事件循环将开始下一轮事件循环,即重新进入定时器阶段。检查阶段setImmediate()的回调会加入到检查队列中。从事件循环的阶段图可以知道,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)/*timer1promise1timer2promise2inbrowser*//*timer1timer2promise1promise2innode*/constfs=require('fs')fs.readFile('test.txt',()=>{console.log('readFile')setTimeout(()=>{console.log('timeout')},0)setImmediate(()=>{console.log('immediate')})})/*readFileimmediatetimeout*/moreexampleslibuvsourcehttps://github.com/libuv/libu...其他requestAnimationFrameHTML5标准规定setTimeout()的最小值第二个参数的(最短间隔)不能低于4毫秒,低于这个值会自动增加。在此之前,旧版浏览器将最小间隔设置为10毫秒。另外,对于那些DOM变化(尤其是那些涉及页面重新渲染的),它们通常不会立即执行,而是每16毫秒执行一次。这时候使用requestAnimationFrame()的效果比setTimeout()要好。客户端可以实现包含鼠标键盘事件的任务队列,以及其他任务队列,鼠标键盘事件的任务队列有更高的优先级,比如有75%的可能性执行。这样既可以保证流畅的交互,也可以进行其他的任务。但是,同一个任务队列中的任务必须按照先进先出的顺序执行。用户点击和button.click()的区别:用户点击:依次执行监听器。浏览器不知道有多少个listener,就找一个执行一个,执行完再看还有没有。点击:同步执行监听器。click方法会先收集有哪些监听器,然后依次触发。Promise的队列与setTimeout的队列有什么关系?Browser'sEventLoopEventLoops深入理解js事件循环机制(Node.js篇)JavaScript运行机制详解:浅谈EventLoopNode.js事件循环、定时器和process.nextTick()
