这是我对EventLoop的理解1.前言众所周知,在使用javascript的时候,往往需要考虑程序中异步的存在。如果异步考虑不周,在开发过程中很容易出现技术错误。和业务错误。作为一个合格的javascript用户,了解异步的存在和运行机制是非常重要和必要的;那么,什么是异步?不得不提EventLoop:也叫事件循环,是指在浏览器或者Node环境中解决javaScript单线程运行时非阻塞问题的一种机制,即实现异步的原理。javascript作为一种单线程语言,本身并没有异步这个词,是由它的宿主环境提供的(网上有很多关于EventLoop的优秀文章,这篇文章是我自己综合理解的)。注意:ECMAScript标准中没有定义EventLoop,而是在HTML标准中;2.EventLoop知识铺垫javascript代码运行时,任务分为两种,宏任务(MacroTask/Task)和微任务(MircoTask);EventLoop在执行和协调各种任务时,也将任务队列分为TaskQueue和MircoTakQueue,分别对应管理宏任务(MacroTask/Task)和微任务(MircoTask);作为队列,TaskQueue和MircoTakQueue也具有队列的特点:先进先出(FIFO——先进先出)。1、微任务(MircoTask)HTML标准中并没有明确规定Microtask,但实际开发中包括以下四种:then、catch、finallyinPromise(原理参考:【js进阶】手撕Promise,一段代码一个分析包看懂)MutationObserver(监听DOM变化的API,详见MDN)Object.observe(废弃:监听标准对象的变化)Process.nextTick(Node环境,通常也认为是微任务)2.MacroTask(MacroTask/Task)基本上,我们将javascript中的所有非微任务(MircoTask)任务归类为宏任务,例如:脚本DOM操作中的所有代码用户交互操作所有网络请求定时器相关的setTimeout、setInterval等··3.javascriptruntimejavascriptruntime:为JavaScript提供一些对象或机制使其能够与外界进行交互,是javascript的执行环境。javascript执行时,会创建一个主线程和call-stack调用栈(executionstack,遵循后进先出的规则),所有任务都会放到callstack/executionstack中等待主线程执行。其运行机制如下:1)主线程自上而下依次执行所有代码;2)同步任务直接进入主线程执行;3)异步任务进入EventTable,当异步任务有结果时,对应注册回调函数,放入EventQueue;4)主线程任务执行完空闲后,从事件队列(FIFO)中读取任务,放入主线程执行;5)放入主线程的EventQueue任务继续从第一步开始,这样执行;以上步骤的执行过程就是我们所说的事件循环(EventLoop)。上图展示了事件循环中一个完整的循环过程。3.浏览器环境下的EventLoop在不同的执行环境中,EventLoop的执行机制是不同的;例如,Chrome和Node.js都使用V8引擎:V8实现并提供了ECMAScript标准中的所有数据类型和运算符、对象和方法(注意没有DOM)。但是它们的运行时是不同的:Chrome提供了window和DOM,而Node.js需要,process等等。在了解EventLoop在浏览器中的具体表现之前,我们需要梳理一下同步、异步、微任务、宏任务的关系!1.同步、异步、宏任务、微任务看到这里,你可能会有很多疑惑:同步和异步很好理解,宏任务和微任务也有分类,但是放在一起,感觉很混乱。我感觉同步异步和宏任务、微任务是有内在联系的,但是他们之间有联系吗?有什么联系?网上有文章说宏任务是同步的,微任务是异步的。这种说法显然是错误的!其实我更喜欢这样描述:宏观任务和微观任务是相对的。根据代码执行循环的先后顺序,对代码执行进行分层理解。在每一层(一层)事件循环中,先看整体代码块做一个宏任务,宏任务中的Promise(then,catch,finally),MutationObserver,Process.nextTick是宏任务层的微任务;宏任务中的同步代码进入主线程立即执行,非微任务异步执行代码将进入调用栈等待作为下一个周期的宏任务执行;此时调用栈中等待执行的队列分为两种,先执行优先级较高的本层循环微任务队列(MicroTaskQueue),优先级较低的宏任务队列(MacroTaskQueue)优先低级循环执行!注:每个循环/层都以宏任务开始,以微任务结束;2.简单实例分析上面的描述比较难发音,结合代码和图片分析理解:暂时不给出答案,我们先分析一下代码:这是一个简单典型的事件循环执行案例一个双层循环。在这个循环中,可以按照以下步骤进行分析:1.首先区分本层宏任务的范围(整个代码);2.区分宏任务同步代码和异步代码同步代码:console.log('scriptstart');,console.log('enterpromise');和console.log('脚本结束');异步代码块:Promise的setTimeout和then(注:只有Promise中then、catch、finally的执行需要等待结果,Promise传入的回调函数属于同步执行代码);3.在异步中找到同层的微任务(代码中是Promise的)和下层事件循环的宏任务(代码中是setTimeout)4.宏任务的同步代码先进入主线程,然后以自上而下的顺序执行;输出序列是://同步代码执行lineoutputscriptstartenterpromisescriptend5.主线程空闲时,执行本层microtask//同层microtask队列代码执行,输出promisethen1promisethen26.第一层事件循环结束,进入事件第二层循环(包含在setTimeout执行代码中,只有一个同步代码)//第二层宏任务队列代码执行输出setTimeout综合分析,最终得到数据结果://第一层宏任务代码执行输出脚本startenterpromisescriptend//第一层微任务队列代码执行输出promisethen1promisethen2//第二层宏任务队列代码执行输出setTimeout3,复杂案例分析那么,上面的执行过程你已经看懂了吗?如果你完全理解了上面的例子,就意味着你已经了解了浏览器中EventLoop的执行机制。但是,如果你想知道你是否完全理解它,你不妨分析和测试以下多周期事件循环并给出你的结果:console.log('1');setTimeout(function(){console.log('2');newPromise(function(resolve){console.log('3');resolve();}).then(function(){console.log('4')})setTimeout(function(){console.log('5');newPromise(function(resolve){console.log('6');resolve();}).then(function(){console.log('7')})})console.log('14');})newPromise(function(resolve){console.log('8');resolve();}).then(function(){console.log('9')})放超时(function(){console.log('10');newPromise(function(resolve){console.log('11');resolve();}).then(function(){console.log('12')})})console.log('13')分析:如下草图所示,左上角标记a为宏任务队列,左上角标记i为宏任务队列微任务队列。先执行,再执行microtask;本层宏任务中的非微任务异步代码块作为下层循环的宏任务进入下一个循环,在本循环中执行;如果你的结果与下面的结果一致,恭喜你的浏览器环境你已经完全掌握了EventLoop,那么请开始下面的学习:1->8->13->9->2->3->14->4->10->11->12->5->6->74.Node环境下的EventLoop在Node环境下,浏览器的EventLoop机制是不适用的,切记不要混淆。这里摘自网上很多博客的总结(其实我是真的不懂):NodeEventLoopisimplementedbasedonlibuv:libuvisanewcross-platformabstractionlayerforNode.libuv采用异步和事件驱动编程,核心是提供I/O事件循环和异步回调。libuv的API包括计时、非阻塞网络、异步文件操作、子进程等。1.EventLoop的6个阶段Node的Event循环分为6个阶段,每个阶段具体如下:timers:执行setTimeout和setInterval中的过期回调。Pendingcallback:上一个周期的少量回调会在这个阶段执行。空闲,准备:仅供内部使用。poll:最重要的阶段,执行pendingcallback,在合适的情况下阻塞回到这个阶段。check:执行setImmediate的回调。closecallbacks:执行关闭事件的回调,比如socket.on('close'[,fn])或者http.server.on('close,fn)。注意:以上六个阶段不包括process.nextTick()关键:如上图所示,在Node.js中,一个macrotask可以认为是包含了以上六个阶段,而microtask微任务会介于各个eventloopExecution的stages,即执行完一个stage后,microtask队列中的任务就会被执行。2.process.nextTick()在第二节中,我们了解到process.nextTick()属于microtasks,但是这里需要说明一下:process.nextTick()虽然是异步API的一部分,但是在中显示的数字。因为process.nextTick()在技术上不是事件循环的一部分;当每个阶段完成后,如果有nextTick,队列中的所有回调函数将被清除并优先于其他微任务执行(可以理解为微任务中优先级最高的)3.实例分析老规矩,在线代码:console.log('1');setTimeout(function(){console.log('2');process.nextTick(function(){console.log('3');})newPromise(function(resolve){console.log('4');resolve();}).then(function(){console.log('5')})})process.nextTick(function(){console.log('6');})newPromise(function(resolve){console.log('7');resolve();}).then(function(){console.log('8')})setTimeout(function(){console.log('9');process.nextTick(function(){console.log('10');})newPromise(function(resolve){console.log('11');resolve();}).then(function(){console.log('12')})})console.log('13')将执行代码分区解读分析:如下图草稿图,标记a左上角是宏任务队列,左上角标记i是微任务队列,左上角标记t是定时器阶段队列,左上角标记p是nextTick队列在同层循环。先执行本层的宏任务,再执行微任务;本层宏任务中的非微任务异步代码块作为下层循环的宏任务进入下一个循环,循环执行如下:1.整体代码可以看作是一个宏任务。同步代码直接进入主线程执行,输出1、7、13,然后执行同层的microtasks,nextTick先执行输出6、8;setTimeout代码块在6个阶段进入定时器阶段,依次进入有t1和t2的队列;代码等效于:setTimeout(function(){console.log('2');process.nextTick(function(){console.log('3');})newPromise(function(resolve){console.log('4');resolve();}).then(function(){console.log('5')})})setTimeout(function(){console.log('9');process.nextTick(function(){console.log('10');})newPromise(function(resolve){console.log('11');resolve();}).then(function(){console.log('12')})})3、setTimeout中的同步代码立即执行,输出2、4、9、11,nextTick和Pormise。然后进入microTask执行输出3、10、5、12;4.二楼6个舞台中没有其他舞台。循环完成后,最终输出为:1->7->13->6->8->2->4->9->11->3->10->5->12;4.测验console.log('1');setTimeout(function(){console.log('2');process.nextTick(function(){console.log('3');})newPromise(function(分辨率ve){console.log('4');解决();}).then(function(){console.log('5')setTimeout(function(){console.log('6');process.nextTick(function(){console.log('7');})newPromise(function(resolve){console.log('8');resolve();}).then(function(){console.log('9')})})})})process.nextTick(function(){console.log('10');})newPromise(function(resolve){console.log('11');resolve();}).then(function(){console.log('12')setTimeout(function(){console.log('13');process.nextTick(function(){console.log('14');})newPromise(function(resolve){console.log('15');resolve();}).then(function(){console.log('16')})})})setTimeout(function(){console.log('17');process.nextTick(function(){console.log('18');})newPromise(function(resolve){console.log('19');resolve();}).然后(函数(){console.log('20')})})控制台。log('21')五、总结在浏览器和Node环境下,microtask任务队列的执行时机不同:在Node端,microtask在事件循环的各个阶段之间执行;在浏览器端,microtask在事件循环的macrotask执行完之后执行;参考本次深入理解JavaScriptEventLoop学习,彻底理解JavaScript执行机制【THELASTTIME】彻底理解JavaScript执行机制
