当前位置: 首页 > 后端技术 > Node.js

吃透浏览器Event-loop

时间:2023-04-04 00:55:40 Node.js

为什么要写这篇博文?前段时间跟今日头条的朋友聊天,问今日头条面试前端会问什么问题。他说如果是他的面试,event-loop肯定会问。那天聊了很多,event-loop给我留下了很深的印象。原因很简单,因为我之前对它的了解从来没有深入过。如果我在面试的时候遇到这个问题,我估计答案肯定不如Satisfactory。因此,最近看了一些相关的文章,仔细整理,输出这篇博文,希望能帮助大家理解浏览器的事件循环。node中的event-loop会在后面补充。一、预备知识JavaScript的运行机制:(1)所有的同步任务都在主线程上执行,形成一个执行上下文栈。(2)除了主线程之外,还有一个“任务队列”。只要异步任务有运行结果,就会在“任务队列”中放入一个事件。(3)一旦“执行栈”中的所有同步任务都执行完毕,系统就会去读取“任务队列”,看看里面有什么事件。那些对应的异步任务结束等待状态,进入执行栈,开始执行。(4)主线程不断重复上面的第三步。总结就是:调用栈中的同步任务都执行完了,清栈,也就是主线程空闲了。这时,它会去任务队列中按顺序读取一个任务,然后入栈执行。每次栈清空时,都会读取任务队列中是否有任务,有则读取执行,循环读取执行。一个事件循环中有一个或多个任务队列。JavaScript中有两种异步任务。:宏任务:script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI渲染微任务:process.nextTick(Nodejs)、Promises、Object.observe、MutationObserver;2.事件循环(event-loop)是什么?主线程从“任务队列”中读取执行事件。这个过程是循环的。这种机制称为事件循环。机制是这样的:主线程会不断的从任务队列中取出任务按顺序执行,每执行完一个任务就检查微任务队列是否为空(执行一个任务的具体标志是函数执行栈是empty),如果不是Empty将立即执行所有微任务。然后进入下一个循环,从任务队列中取出下一个任务执行。详细说明:选择当前要执行的宏任务队列,选择一个最先进入任务队列的宏任务,如果没有宏任务可以选择,则跳转到微任务的执行步骤。将事件循环的当前运行的宏任务设置为选定的宏任务。运行宏任务。将事件循环当前运行的任务设置为null。从宏任务队列中移除完成的宏任务。微任务步骤:进入微任务检查点。更新界面渲染。返回第一步。进行进入微任务检查的具体步骤如下:设置进入微任务检查点的标志为真。当事件循环的微任务队列不为空时:选择一个最先进入微任务队列的微任务;将事件循环的当前运行任务设置为选定的微任务;运行微任务;将事件循环当前运行的任务设置为null;它将运行完成的微任务从微任务队列中移除。对于相应事件循环的每个环境设置对象,通知它们哪些承诺被拒绝。清理indexedDB事务。将进入微任务检查点的标志设置为false。需要注意的是,当前执行栈执行完毕后,会立即处理microtask队列中的所有事件,然后再从macrotask队列中取出一个事件。在同一个事件循环中,微任务总是在宏任务之前执行。插图:3.Event-loop是如何工作的?让我们看一个简单的例子:setTimeout(()=>{console.log("setTimeout1");Promise.resolve().then(data=>{console.log(222);});});setTimeout(()=>{console.log("setTimeout2");});Promise.resolve().then(data=>{console.log(111);});想一想,运行的结果是什么?运行结果为:111setTimeout1222setTimeout2我们看看为什么?下面详细解释一下JS引擎是如何执行这段代码的:主线程上没有要执行的代码然后遇到setTimeout0,它的作用是在0ms后将回调函数放入宏任务队列中(这个任务说明下面在事件循环中执行)。然后遇到setTimeout0,它的作用是在0ms后将回调函数放入宏任务队列中(这个任务会在下一个事件循环中执行)。首先查看microtask队列,即microtask队列,发现队列不为空,执行第一个promise的then回调,输出'111'。此时microtask队列为空,进入下一个事件循环,查看macrotask队列,找到一个setTimeout回调函数,立即执行回调函数输出'setTimeout1',查看microtask队列,发现队列不为空,执行promisethencallback,输出'222',微任务队列为空,进入下一个事件循环。查看宏任务队列,发现有setTimeout的回调函数,立即执行回调函数,输出'setTimeout2'。再想想下面代码的执行顺序:console.log('scriptstart');setTimeout(function(){console.log('setTimeout---0');},0);setTimeout(function(){console.log('setTimeout---200');setTimeout(function(){console.log('inner-setTimeout---0');});Promise.resolve().then(function(){console.log('promise5');});},200);Promise.resolve().then(function(){console.log('promise1');}).then(function(){console.log('promise2');});Promise.resolve().then(function(){console.log('promise3');});console.log('脚本结束');想一想,运行的结果是什么?运行结果为:scriptstartscriptendpromise1promise3promise2setTimeout---0setTimeout---200promise5inner-setTimeout---0Sowhy?下面详细解释一下JS引擎是如何执行这段代码的:首先,主进程上的同步任务是顺序执行的,然后第一句和最后一句的console.log遇到setTimeout0。将回调函数放入宏任务队列(此任务将在下一个事件循环中执行)。然后遇到setTimeout200,它的作用是200ms后将回调函数放入宏任务队列中(这个任务会在下一个事件循环中执行)。同步任务执行完后,首先查看microtask队列,即microtask队列,发现队列不为空,执行第一个promise的then回调,输出'promise1',然后执行第一个promise的then回调第二个promise,并输出'promise3'',由于第一个promise的.then()的返回仍然是一个promise,所以第二个.then()会被放入microtask队列中继续执行,输出'promise2';此时microtask队列为空,进入下一个事件循环,查看宏任务队列,找到setTimeout的回调函数,立即执行回调函数并输出'setTimeout---0',查看microtask队列,队列为空,进入下一个事件循环。查看宏任务队列,发现有setTimeout回调函数,立即执行回调函数,输出'setTimeout---200'。然后遇到setTimeout0,它的作用是在0ms后将回调函数放入宏任务队列,查看微任务队列,即微任务队列,发现这个队列不为空,则执行promise的then回调和输出“承诺5”。此时microtask队列为空,进入下一个事件循环,查看macrotask队列,找到一个setTimeout回调函数,立即执行回调函数输出,输出'inner-setTimeout---0'。代码执行结束。4.为什么?需要一个事件循环?因为JavaScript是单线程的。单线程是指所有的任务都需要排队,上一个任务完成后才会执行下一个任务。如果前一个任务耗时很长,后一个任务就得一直等下去。为了协调事件(event)、用户交互(userinteraction)、脚本(script)、渲染(rendering)、网络(networking)等,用户代理(useragent)必须使用事件循环(eventloops)。5.参考文章:https://segmentfault.com/a/11...https://segmentfault.com/a/11...https://segmentfault.com/a/11...http:///www.ruanyifeng.com/blo...感谢您花费宝贵的时间阅读本文。如果这篇文章给了你一点帮助或者启发,那就不要吝啬你的点赞和star。您的肯定是我前进的最大动力。https://github.com/YvetteLau/...推荐关注我公众号: