写前感想:在各大网站、博客当中,写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当作一个宏任务)、setTimeout、setInterval、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN)、I/O、UI Rendering。
微任务:Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver(具体使用方式查看这里)
事件循环的过程
终于终于要到了讲这个循环是怎么样的一个过程,没办法,前面那些都是我们要知道的前置知识,知道了上面那些,我们再来体会上面那句话,事件循环的作用方式是,通过监控执行栈和任务队列,如果执行栈是空的,就从任务队列当中取出任务压入到执行栈当中执行。
具体的过程如下:
Event Loop中,每一次循环称为tick,每一次tick的任务细节如下:
- 调用栈栈选择最先进入队列的宏任务(MacroTask)(通常是script整体代码),如果有则执行;
- 检查是否存在 微任务(MicroTask),如果存在则不停的执行,直至清空微任务队列(MicroTask Queue) ;
- 浏览器更新渲染(render),每一次事件循环,浏览器都可能会去更新渲染;
- 重复以上步骤。
如果你第一次看,可能会很迷👀,不过没关系,请听我细细道来:
首先,执行全局Script代码,这些同步代码有一些是同步语句,有一些是异步语句;异步语句放入相应的任务队列,当执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去执行Task(宏任务),每次宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。
分步骤来看:
第一:执行全局Script代码;
第二:全局Script执行完成之后,执行栈会清空;
第三:执行栈为空,从宏任务队列取出一个任务;
第四:检查是否存在微任务,如果有就执行,直至清空
第五:渲染;
然后重复三、四、五…….
所以你可以把全局的Script代码看成一个宏任务;那每一次循环就完全按照tick的3个过程;
废话不多说,上图
(求生欲:盗来的一张图,原文链接放下面了🐱🐉🐱🐉)
或许,可能,你还有疑问;
不是大家说的是先微后宏嘛???
不是一个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)
参考文章: