最近对Eventloop比较感兴趣,所以学习了一下。但是我发现,虽然整个Eventloop的文章很多,但是却没有一篇文章可以看懂它的全部内容。大多数文章只解释浏览器或Node.js中的一种。不对比而去看,总会有浅薄的认识。这就是为什么我有这篇文章,从一百个思想流派中挑选出最好的。1.定义Eventloop:为了协调事件(event)、用户交互(userinteraction)、脚本(script)、渲染(rendering)、网络(networking)等,用户代理(useragent)必须使用事件循环(eventloops)。(3月29日修订)那么什么是事件?事件:事件是一些外部或内部信息状态的变化,从而导致相应的反应。例如,当用户点击一个按钮时,它是一个事件;当加载HTML页面时,它也是一个事件。一个事件包含多个任务。我们在之前的文章中提到过,JavaScript引擎,又称JavaScript解释器,是一种将JavaScript解释成机器码的工具,分别运行在浏览器和Node中。根据上下文的不同,事件循环也有不同的实现:Node使用libuv库来实现事件循环;在浏览器中,html规范定义了事件循环,具体实现留给不同的厂商来完成。因此,浏览器的事件循环和Node的事件循环是两个概念,我们分开来看。2.含义在实际工作中,理解Eventloop的含义可以帮助你分析一些异步序列问题(当然,随着es7async和await的普及,这样的机会越来越少了)。另外,对你理解浏览器和Node的内部机制也有积极的作用;面试的时候,当你被问到一堆异步操作的执行顺序时,你不会被蒙蔽。3、在浏览器上的实现在JavaScript中,任务分为两种:Task(也称为MacroTask,宏任务)和MicroTask(微任务)。它们分别包含以下内容:MacroTask:script(整体代码)、setTimeout、setInterval、setImmediate(节点唯一)、I/O、UI渲染MicroTask:process.nextTick(节点唯一)、Promises、Object.observe(deprecated),MutationObserver需要注意的一点是,在同一个上下文中,整体的执行顺序是同步代码—>microTask—>macroTask[6]。我们将在下面讨论这部分。在浏览器中,一个事件循环中有很多来自不同任务源的任务队列(taskqueues),每个任务队列中的任务都是按照严格的先进先出顺序执行的。但是由于浏览器自身的调度,不同任务队列中任务的执行顺序是不确定的。具体来说,浏览器会继续从任务队列中按顺序执行任务,并在每个任务执行完后检查微任务队列是否为空(执行任务的具体标志是函数执行栈为空),如果为空不为空所有微任务将同时执行。然后进入下一个循环,从任务队列中取出下一个任务执行,以此类推。注意:图中橙色的MacroTask任务队列应该也是不断切换的。本段大量引用《什么是浏览器的事件循环(Event Loop)》的相关内容,如果想看更详细的说明,可以自行使用。4、Node上实现的nodejs事件循环分为6个阶段,会按顺序重复运行,如下:timers:在setTimeout()和setInterval()中执行过期回调。I/O回调:上一个周期的少量I/O回调会延迟到本轮这个阶段执行idle,prepare:移动队列,内部只使用poll:最重要的阶段,执行I/Ocallback,在合适的条件下,会在这个阶段阻塞check:执行setImmediate的回调closecallbacks:执行close事件的回调,比如socket.on("close",func)和浏览器不同,在每个阶段之后完成,而不是完成MacroTask任务,将执行microTask队列。这会导致相同的代码在不同的上下文中得到不同的结果。我们将在下面探讨。另外需要注意的是,如果setImmediate是在timers阶段执行的时候创建的,那么会在本次循环的check阶段执行。如果setTimeout是在timers阶段创建的,由于已经取出了timers,所以会进入下一个循环,和check阶段创建timers任务是一样的。5.例子5.1浏览器和Node执行顺序的区别,0)setTimeout(()=>{console.log('timer2')Promise.resolve().then(function(){console.log('promise2')})},0)浏览器输出:time1promise1time2promise2Node输出:time1time2promise1promise2在这个例子中,Node的逻辑如下:最初timer1和timer2处于timers阶段。一开始先进入timers阶段,执行timer1的回调函数,打印timer1,并将promise1.then回调放入microtask队列,同步执行timer2,打印timer2;至此,定时器阶段执行完毕,事件循环进入下一阶段,执行微任务队列中的所有任务,依次打印promise1和promise2。浏览器使用两个setTimeout作为两个MacroTask,所以先输出timer1和promise1,再输出timer2和promise2。更详细的信息可以在《深入理解js事件循环机制(Node.js篇)》中找到为了证明我们的理论,将代码更改为以下内容:setImmediate(()=>{console.log('timer1')Promise.resolve().then(function(){console.log('promise1')})})setTimeout(()=>{console.log('timer2')Promise.resolve().then(function(){console.log('promise2')})},0)节点输出:timer1timer2promise1orpromise2timer2timer1promise2promise1按理说setTimeout(fn,0)应该比setImmediate(fn)快,应该只有第二种结果。为什么会有两个结果?这是因为Node做不到0毫秒,至少需要1毫秒。在实际执行中,进入事件循环后,可能是1毫秒,也可能不是1毫秒,取决于当时系统的状态。如果小于1毫秒,则跳过timers阶段,进入check阶段,先执行setImmediate的回调函数。另外,如果Timer阶段已经过去,那么setImmediate会比setTimeout更快,例如:constfs=require('fs');fs.readFile('test.js',()=>{setTimeout(()=>console.log(1));setImmediate(()=>console.log(2));});上面的代码会先进入I/O回调阶段,然后是check阶段,最后是timers阶段。所以setImmediate会比setTimeout早执行。详情见《Node 定时器详解》。5.2不同异步任务的执行速度setTimeout(()=>console.log(1));setImmediate(()=>console.log(2));Promise.resolve().then(()=>console.log(3));process.nextTick(()=>console.log(4));Outputresult:4312or4321因为我们上面说了microTask会比macroTask运行的好,所以先输出如下二、在Node[3]中process.nextTick比Promise更受欢迎,所以4在3之前。根据我们之前所说的,Node没有绝对意义上的0ms,所以1和2的顺序不固定.5.3MicroTask队列和MacroTask队列setTimeout(function(){console.log(1);},0);控制台日志(2);process.nextTick(()=>{console.log(3);});newPromise(function(resolve,rejected){console.log(4);resolve()}).然后(res=>{console.log(5);})setImmediate(function(){console.log(6)})console.log('end');节点输出:24end3516本例来自《JavaScript中的执行机制》。Promise的代码是同步代码,而then和catch是异步的,所以4应该是同步输出的,然后Promise的then位于microTask中,优于macroTask队列中的其他任务,所以5会比1好,6输出,而Timer优于Check阶段,所以1,6。6.小结综上所述,关于最关键的顺序,我们要遵循以下规则:在相同的上下文下,MicroTask先于MacroTask运行,然后浏览器按照一个MacroTask任务运行,所有MicroTask都运行在order,Node会按照六个stage依次运行,每个stage之后都会运行MicroTask队列。同样的MicroTask队列下,process.tick()会比PromiseEvent循环更好。还是比较深奥的,深入下去会有很多有趣的东西。如果您有任何问题,请随时指出。.参考文献:《什么是浏览器的事件循环(Event Loop)》《不要混淆nodejs和浏览器中的event loop》《Node 定时器详解》《浏览器和Node不同的事件循环(Event Loop)》《深入理解js事件循环机制(Node.js篇)》《JavaScript中的执行机制》
