看完一定懂的 Event Loop

4,552 阅读10分钟

写前感想:在各大网站、博客当中,写Event Loop的文章实在是太多了,但其实只要深入的去理解某一篇,你会发现它也不过如此;也不用看太多,因为大家的理解的水平都大差不差,写的都是那点东西;重要的是,当你真正理解之后,可以对于当中某一些细小的问题,进行深入研究与探讨,这才是你真正的收获。

废话了那么多,我其实想说的是,本文也是一篇关于Event Loop,平平无奇的小文章,但只要你用心去看,会让你看懂是怎么回事;如果面试的话,我也在最后提供了我自己的回答模板;但是如果想要了解很深邃的东西….啊,恕我无能,臣妾还做不到!!!

What is Event Loop?

Event Loop 也就是事件循环,我们先抛开它到底是怎么循环的,来谈谈为什么会有这个东西。

首先,我们知道的是JS是单线程的(至于为什么是单线程的,还不知道人请看注释1;温馨提示:点击右侧目录更方便),那既然只有一个线程来执行事件,就会产生一些问题,就比如一个场景,假如现在小张这边有一些任务,分别是看书、写作业、吃外卖、做广播体操;正常情况下,小张是写完作业之后,点一个外卖,等外卖送过来吃掉,再做广播体操。在这个过程中,小张其实会有一个“干等”的时间🤔,也就是什么都不做,在那等外卖的时间。

单线程的JS和小张做事情是相似的,那小张比较懒,可以干等着,但是如果JS也干等着,那对于用户来说,这就发生了阻塞了呀,万万不可!!🤷‍♀️(准确一点来说,JS也没有干等着,它是自己在做那些耗时的事情,类比到这里就是自己去店里拿外卖)

这个时候Event Loop就站出来了,它是一种可以让JS既是单线程,又不会阻塞的机制;人话就是,他通过一些手段,让JS执行事件的效率更高;既然你有一些耗费时间的事情,那就先把这些事情放在一边,先做立即可以完成的,等到那些耗时的事情有结果了,再去拿那个结果;到小张这里就是,点完外卖之后就开始做广播体操,做完广播体操,再吃送过来的外卖。

专业一点来讲就是,Event Loop是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。作用的方式就是,通过监控执行栈和任务队列,如果执行栈是空的,就从任务队列当中取出任务压入到执行栈当中执行(后文会详解),所以,他其实也就是一种异步的实现机制。(至于异步回调还有哪些机制,还不知道人请看注释2;温馨提示:点击右侧目录更方便)

知道Event Loop 就要先知道的几个概念

同步事件与异步事件

同步事件指的是任务一件一件的完成,语句从上到下一条一条的执行,执行完成再执行下一条语句。就比如一条简简单单的console.log(xxx),就执行到它,打印就行,打印完了,就继续往下执行。对于小张来说,就是看书,写作业,做广播体操;

而异步事件是相对于同步来讲的,就是一些比较耗费时间的,它需要执行一系列的操作,但我们最后其实只需要知道它的结果就ok了,所以我们会把它执行的过程先挂起来,不让它影响我们后续要做的事情。就是把拿外卖的事情交给外卖员来做,小张继续做广播体操,最后再去吃外卖。

执行栈

(JS内存模式那张图我就不放了,随便点开一篇Event Loop的文章就有;意思就是JS内存中会有堆、执行栈、任务队列…)

执行栈会将当前的执行上下文(通俗一点可以理解成当前的函数调用)压入到执行栈当中,执行完成后就会把它弹出去。

Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。

鄙人不才,十分粗糙地写了一篇小文章执行上下文与作用域 - 掘金 (juejin.cn),有兴趣的友友,可以通过这篇文章,简单了解一下执行栈入栈出栈的过程。

任务队列

任务队列就是存放异步任务的队列,也没有什么特别的。但是在JavaScript当中,有两种任务队列,一个是宏任务队列,一个是微任务队列。我们只需要知道哪些是宏任务、哪些是微任务,然后对应类型的任务放到相应的任务队列当中。

宏任务:script全部代码(指得是你可以把一整个大的script当作一个宏任务)、setTimeoutsetIntervalsetImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/OUI Rendering

微任务:Process.nextTick(Node独有)PromiseObject.observe(废弃)MutationObserver(具体使用方式查看这里

事件循环的过程

终于终于要到了讲这个循环是怎么样的一个过程,没办法,前面那些都是我们要知道的前置知识,知道了上面那些,我们再来体会上面那句话,事件循环的作用方式是,通过监控执行栈和任务队列,如果执行栈是空的,就从任务队列当中取出任务压入到执行栈当中执行。

具体的过程如下:

Event Loop中,每一次循环称为tick,每一次tick的任务细节如下:

  • 调用栈栈选择最先进入队列的宏任务(MacroTask)(通常是script整体代码),如果有则执行;
  • 检查是否存在 微任务(MicroTask),如果存在则不停的执行,直至清空微任务队列(MicroTask Queue) ;
  • 浏览器更新渲染(render),每一次事件循环,浏览器都可能会去更新渲染;
  • 重复以上步骤。

如果你第一次看,可能会很迷👀,不过没关系,请听我细细道来:

首先,执行全局Script代码,这些同步代码有一些是同步语句,有一些是异步语句;异步语句放入相应的任务队列,当执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去执行Task(宏任务),每次宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。

分步骤来看:

第一:执行全局Script代码;

第二:全局Script执行完成之后,执行栈会清空;

第三:执行栈为空,从宏任务队列取出一个任务;

第四:检查是否存在微任务,如果有就执行,直至清空

第五:渲染;

然后重复三、四、五…….

所以你可以把全局的Script代码看成一个宏任务;那每一次循环就完全按照tick的3个过程;

废话不多说,上图

image.png

(求生欲:盗来的一张图,原文链接放下面了🐱‍🐉🐱‍🐉)

或许,可能,你还有疑问;

不是大家说的是先微后宏嘛???

不是一个Event Loop会有一个或多个宏任务队列(MacroTask Queue),只一个微任务队列(MicroTask Queue)嘛?你这个图怎么反了???

对于上面两个问题,我的答案是 “是的,是的,确实是的”

其实是这样的,每个宏任务队列 (MacroTask Queue) 都保证按照回调函数(callback)入队列的顺序依次执行宏任务(MacroTask),在宏任务( MacroTask )或者微任务(MicroTask)中产生的新微任务(MicroTask) 会被压入到微任务队列(MicroTask Queue)中并执行。而在执行两个宏任务( MacroTask )之间,也即在执行下一个宏任务( MacroTask )之前,会优先执行完所有微任务(MicroTask),也即会优先清空已有的微任务队列(MicroTask Queue) 。因此,图中第二个微任务队列(MicroTask Queue)产生的时候,第一个微任务微任务队列(MicroTask Queue)其实已经被清空了。所以Event Loop实际上仅有一个微任务队列(MicroTask Queue)。

附上我自己的理解:不太科学,所以仅供参考😜

在我的理解中是这样的,你可以把每个宏任务看成一个大家庭,这个家庭里有同步代码、微任务、甚至宏任务;进入主进程的方式是把这些家庭一个一个按顺序塞进去的,每塞进去一个宏任务,就执行。在执行的过程中,就按顺序执行,同步代码就执行掉;遇到自己的微任务,就添加到它的微任务队列当中,里面的宏任务会加入到大家庭的后面,形成新的大家庭。同步代码执行完成之后,就检查微任务队列,执行微任务代码。全部执行完之后,进行一次页面渲染。然后再把下一个大家庭塞进主进程当中,重复过程。

我的面试模板

😒当面试官面无表情地说:讲一下Event Loop吧.

😊我:JS是单线程的,但是我们在写代码的时候,会有同步执行的代码和异步执行的代码。EventLoop就是一种解决异步回调的一种机制。具体的解决办法就是使用一个执行栈和事件队列,事件队列又分为宏任务队列和微任务队列。简单的来讲,就是把代码从上到下,会把同步任务压入到执行栈,遇到异步的任务,根据异步任务的类型,放入不同的事件队列,交给其它线程进行处理。如果执行栈空的话,就从事件队列当中取出结果,放入到执行栈中执行并执行。Event Loop 的每一次循环称为一个tick,具体是先拿出一个宏任务,然后检查它里面的微任务,如果有的话,就执行所有的微任务,结束之后,进行一次渲染。再拿出一个宏任务,按照刚刚的过程继续进行。

注释

注释1:JS为什么是单线程的?

答:因为JS创立的本质就是就简单的操作DOM,完成用户的一些交互。如果是多个线程的话,那就有可能产生冲突的情况,例如同时又两个线程对一个DOM进行了不同的操作,那要以哪一个线程为准呢?所以为了保持一致性,JS必须是单线程的,也不得不是单线程的。

注释2:异步回调的机制有哪些?

答:回调函数、事件监听、发布订阅者模式、Promise、ES6当中的Generator函数、ES7中async/await;具体内容推荐看关于js中异步问题的解决方案 - 奔跑吧人生 - 博客园 (cnblogs.com)

参考文章:

一次弄懂Event Loop(彻底解决此类面试问题) - 知乎 (zhihu.com)

壹.2.8 Event Loop - 前端内参 (gitbook.io)