当前位置: 首页 > Web前端 > JavaScript

阿里方:熟悉事件循环?然后说说为什么分为宏观任务和微观任务

时间:2023-03-27 15:45:24 JavaScript

什么是事件循环在了解事件循环之前,需要先了解一些JS特性。JS引擎是单线程的。说白了,JS引擎一次只能做一件事,而Java这种多线程语言可以同时做几件事。JS完成的任务分为同步和异步两种。所谓“异步”简单的说就是一个任务不是连续完成的。先执行第一段,准备好了再回去执行第二段。也称为回调;同步是一致完成的。读取文件、网络请求等任务属于异步任务:耗时较长,但中间操作不需要JS引擎自己完成。只需要等别人准备好,把数据给他,然后继续执行回调部分就可以了。如果没有特殊处理,JS引擎应该在执行异步任务时等待,不做任何其他事情。用一张图来展示这个过程,可以看出在执行异步任务时,大量的空闲时间被浪费了。事实上,这是大多数多线程语言的处理方式。但是对于JS这样的单线程语言来说,这种长时间的空闲等待是不可接受的:遇到其他紧急任务,Java可以另开一个线程来处理,而JS却只能忙着等待。因此,采用如下“异步任务回调通知”模式:在等待异步任务准备的同时,JS引擎执行其他同步任务,等待异步任务准备好执行回调。这种模式的优势是显而易见的。完成相同任务所花费的时间大大减少。这种方法也称为非阻塞。实现这个“通知”的是事件循环,将异步任务的回调部分交给事件循环,在合适的时候返回给JS线程执行。事件循环并不是最早的JavaScript,它是计算机的一种运行机制。事件循环由队列组成。异步任务的回调遵循先进先出,在JS引擎空闲的时候会一轮一轮的取出来,所以称为循环。根据队列中任务的不同,分为宏任务和微任务。宏任务和微任务事件循环由宏任务和在执行宏任务期间产生的任何微任务组成。当前macrotask完成后,所有在此期间入队的microtask都会被立即执行。这样设计是为了给紧急任务一个插队的机会,否则新入队的任务会一直排在队尾。区分完microtasks和macrotasks之后,当前循环中的microtasks实际上是在队列中跳转,这样microtasks中所做的状态修改也可以同步到下一轮事件循环中。常见的宏任务包括:script(整体代码)/setTimout/setInterval/setImmediate(nodeonly)/requestAnimationFrame(browseronly)/IO/UIrender(browseronly)常见的微任务有:process.nextTick(node独有)/Promise.then()/Object.observe/MutationObserver宏任务setTimeouterrorsetTimeout的回调可能在指定时间后没有执行。相反,在指定的时间之后,回调函数被放入事件循环的队列中。如果时间到了,JS引擎还在执行同步任务,这个回调函数需要等待;如果当前事件循环队列中还有其他回调,则需要等待其他回调执行完毕。此外,setTimeout0ms不会立即执行,它有一个默认的最小时间4ms。所以下面代码的输出不一定是://nodesetTimeout(()=>{console.log('setTimeout')},0)setImmediate(()=>{console.log('setImmediate')})因为在取出第一个宏任务之前执行全局脚本。如果时间大于4ms,说明setTimeout回调函数已经入队,先执行setTimeout;如果准备时间小于4ms,则先执行setImmediate。浏览器的事件循环浏览器的事件循环由一个宏任务队列+多个微任务队列组成。首先,执行第一个宏任务:全局脚本。生成的宏任务和微任务进入各自的队列。脚本执行后,清空当前微任务队列。完成一个事件循环。然后取出一个宏任务,也把这段时间产生的回调入队。然后清除当前的微任务队列。这来回。只有一个macrotask队列,每个macrotask都有自己的microtask队列,每个cycle由一个macrotask+多个microtask组成。下面的demo展示了微任务的跳队过程:Promise.resolve().then(()=>{console.log('第一个回调函数:微任务1')setTimeout(()=>{console.log('第三个回调函数:宏任务2')},0)})setTimeout(()=>{console.log('第二个回调函数:宏任务1')Promise.resolve().then(()=>{console.log('第四个回调函数:Microtask2')})},0)//第一个回调函数:Microtask1//第二个回调函数:Macrotask1//第四个回调函数:Microtask2//第三个回调函数:macrotask2打印的结果不是从1到4,而是先执行第四个回调函数,然后是第三个,因为是Microtask,比第三个回调函数优先级高。Node的事件循环Node的事件循环比浏览器复杂得多。它由6个宏任务队列+6个微任务队列组成。宏任务从高到低优先级顺序如下:执行规则是:一个宏任务队列执行完后,清空一次微任务队列,往复执行下一级的宏任务队列。宏任务队列与微任务队列配对。六级宏任务全部执行完毕,为一个循环。需要注意的是:Timers、Poll、Check阶段,因为我们写的大部分代码都属于这三个阶段。定时器:定时器setTimeout/setInterval;poll:获取新的I/O事件,比如读取文件的操作等;检查:这里执行了setImmediate回调函数;另外,节点端的微任务还有优先级:process。nextTick;promise.then等;当微任务队列为空时,process.nextTick将首先执行,然后是微任务队列中的其他任务。下面的代码可以证明浏览器和node的区别:console.log('Scriptstart')setTimeout(()=>{console.log('第一个回调函数,宏任务1')Promise.resolve().then(function(){console.log('第四个回调函数,微任务2')})},0)setTimeout(()=>{console.log('第二个回调函数,微任务2')Promise.resolve().then(function(){console.log('第五个回调函数,微任务3')})},0)Promise.resolve().then(function(){console.log('第三个回调函数,microtask1')})console.log('Scriptend')nodeside:ScriptstartsScriptends第三个回调函数,microtask1第一个回调函数,macrotask1第二个回调函数,macrotask2第四个回调函数,microtask2第五个回调函数,微任务3BrowserScriptstartScriptend第三个回调函数,微任务1第一个回调函数,宏任务1第四个回调函数,微任务2第二个回调函数,macrotask2第五个回调函数,microtask3可以看出在node端,必须完成当前层级的所有macrotasks才轮到microtask:第四个回调函数,microtask2在两次setTimeouts后打印完成。因为浏览器执行的时候是一个宏任务+一个微任务队列,而node是一整个宏任务队列+一个微任务队列。node11.x前后版本差异在node11.x之前,其事件循环的规则如前所述:首先取出整个宏任务队列中的所有任务,然后执行一个微任务队列。但是在11.x之后,node端的事件循环变得和浏览器类似:先执行宏任务,再执行微任务队列。但是仍然保留宏任务队列和微任务队列的优先级。可以通过下面的Demo来证明:console.log('Scriptstart')setTimeout(()=>{console.log('宏任务1(setTimeout)')Promise.resolve().then(()=>{console.log('microtaskpromise2')})},0)setImmediate(()=>{console.log('macrotask2')})setTimeout(()=>{console.log('macrotask3(setTimeout)')},0)console.log('脚本结束')Promise.resolve().then(()=>{console.log('microtaskpromise1')})process.nextTick(()=>{console.log('microtasknextTick')})runbeforenode11.x:ScriptstartscriptendmicrotasknextTickmicrotaskpromise1macrotask1(setTimeout)macrotask3(setTimeout)microtaskpromise2macrotask2(setImmediate)Runafternode11.x:Scriptstarts脚本结束microtasknextTickmicrotaskpromise1macrotask1(setTimeout)microtaskpromise2macrotask3(setTimeout)macrotask2(setImmediate)在不同node环境下可以发现:任务队列中的microtaskprocess.nextTick优先级较高,即使晚进入microtask队列,也会打印麦克风rotasknextTick首先是微任务promise1;宏任务setTimeout的优先级高于setImmediate,宏任务2(setImmediate)是三个宏任务中最后打印的;在node11.x之前,微任务队列必须等待当前优先级的所有宏任务先执行,并在两次setTimeout后打印微任务promise2;node11.x之后,微任务队列只需要等待当前宏任务先执行。结语事件循环中的任务分为宏任务和微任务,给高优先级任务一个插队的机会:微任务比宏任务有更高的优先级。节点端的事件循环比浏览器端的要复杂。它的宏观任务分为六个优先级,微观任务分为两个优先级。节点端的执行规则是一个macrotask队列加上一个microtask队列,而浏览器是一个单一的macrotask加上一个microtask队列。但是在node11之后,node和浏览器的规则收敛了。如果您觉得这篇文章对您有帮助,请点个赞~这对我来说很重要