在了解JavaScript的Event Loop之前,咱们先来理解一下几个知识点:
JavaScript单线程
JavaScript是一门单线程语言。
JavaScript的主要用途是与用户交互,而交互过程中时常要操作DOM。咱们假如它是一门多线程的语言,设想一下这样的场景:有两个线程A、B同时操作同一个DOM节点,假如一个线程A某个DOM节点上增加内容,而线程B删除了这个节点,这时浏览器应该以哪个线程的操作为准呢?
因而,JavaScript语言的作者在设计之初基于它的用处和防止复杂性的思考,将它设为一门单线程语言。
浏览器多线程
咱们说JavaScript是单线程的,是指咱们编写的所有JavaScript代码将造成一个个的“工作”,所有的“工作”都须要在主线程上执行,而且是一个工作执行完,才能够执行下一个工作。那么问题来来,当遇到setTimeout、浏览器事件处理、AJAX申请等代码(工作)时,它是怎么执行等呢?如果要始终等到这些工作执行完了再运行前面的代码,那么当这些工作须要很长的工夫才执行完时,将很可能为用户带来蹩脚的体验,导致用户不耐烦地来到你的网站页面甚至一去不复返,这对网站来说将是灾难性的打击。那么有没有什么办法能够不因这些工作而产生期待呢?
原来,浏览器是多线程的,咱们关上一个tab的时候,就开启了一个独立的过程,这个过程蕴含了多个线程:
- JS引擎线程(主线程),负责js代码的解释和执行
- GUI渲染线程,负责页面的绘制
- 定时触发器线程,如咱们编写的setTimeout/setInterval由该线程解决
- HTTP异步申请线程,负责解决网络申请和响应
- 浏览器事件触发线程,解决click、input、scroll等浏览器事件
这样,主线程在遇到setTimeout,AJAX申请的时候,就将它们定义的工作交给对应的线程解决,而后持续运行前面的代码。等到这些工作有了后果或某一事件达到触发条件(如setTimeout设定的工作到了设定的延时、AJAX收回的申请状态变更,用户点击了页面元素等)后再将它们的回调函数增加到相应的工作队列,期待主线程闲暇时将它们取出执行,从而实现了异步。
须要留神的是,JavaScript引擎线程和GUI渲染线程是互斥的,它们其中一个线程的执行会阻塞另一个线程的执行。这也是咱们最好把一些script标签放到body元素开端的起因:当浏览器在加载HTML文件过程中遇到script标签时会停下来(除非你在script中申明了async或defer属性),因为JavaScript代码能够会扭转HTML构造(线程互斥的起因);而JavaScript引擎线程执行JavaScript代码时,GUI渲染线程是不工作的,也就是页面渲染被阻塞了,这当然不是咱们想要的。
Event Loop
进入正题,既然浏览器是多线程的,那么这些线程之间是怎么进行合作的呢?答案是Event Loop。
Event Loop是计算机系统的一种运行机制,浏览器引入Event Loop解决单线程的JavaScript的异步问题
- 浏览器的Event Loop是在html5的标准中明确定义。
- NodeJS的Event Loop是基于libuv实现的。能够参考Node的官网文档以及libuv的官网文档。
- libuv曾经对Event Loop做出了实现,而HTML5标准中只是定义了浏览器中Event Loop的模型,具体的实现留给了浏览器厂商。
咱们说JavaScript是单线程的,就是说任何时候都只有一个线程在运行JavaScript代码,这个线程咱们称之为“主线程”。JavaScript引擎在解析JavaScript代码都时候,把要运行的代码划分成一个个的“工作”,这些代码在主线程上一个一个地执行,造成一个“执行栈”。而这些工作又能够分为两大类:同步工作和异步工作。
同步工作(如JavaScript整体代码)间接放进主线程的执行栈中执行,而异步工作则交由对应的API解决,等到放进异步工作的队列中期待被执行。当执行栈中的工作为空时,就去异步工作的队列中取出工作,放进执行栈中执行,这个过程是一直反复的。整个过程能够如下图所示:
这便是浏览器最简略的Event Loop模型。
宏工作和微工作
而在ES6进去之后,随同着原生Promise的退出,异步工作又能够细分为“宏工作”(macrotask)和“微工作”(microtask),在最新标准中又将它们别离称为tasks和jobs。
异步工作中,属于“宏工作”的有:
- setTimeout,setInterval(它们同属于一个工作源)
- 浏览器事件
- 异步HTTP申请
- requestAnimationFrame
- setImmediate (node.js独有)
- 其余I/O操作如应用FileReader的异步接口等
属于“微工作”的有:
- Promise.prototype.then catch finally
- MutationObserver
- process.nextTick (node.js独有)
依据标准,宏工作的队列能够有多个,而微工作队列只能有一个。并且:
- 在一次循环中,主线程先从宏工作的队列中取出一个(位于队首的)工作放入执行栈执行。
- 执行完一个宏工作(执行栈为空)后,再将微工作队列中的工作顺次取出执行,直到微工作队列为空。
- 这个过程中,如果执行微工作时又产生了新的微工作,会将新的微工作退出微工作队列的开端。
- 最初在进入下一个循环的间隙,由浏览器决定要不要进行UI的渲染,如果须要则进行由GUI渲染线程进行UI渲染操作,渲染实现后告诉主线程进入下一个事件循环;如果不须要UI渲染,则间接进入下一个循环。
这个过程咱们用更具体的一张图示意如下:
到这里,可能人会有问,Event Loop事件循环机制是怎么循环起来的呢?或者更具体的,当Event Loop尝试进入下一个循环,去宏工作队列取工作发现队列为空时,循环不就进行不上来了吗?
笔者认为,如何循环不是本文的重点,加之不同浏览器可能有不同的实现,故不作更多论述。试想,如果是你本人实现,独立一个线程去监听执行栈是否为空也未尝不可,或在没有工作要执行的时候将主线程挂起,等到有新的工作被增加进来再继续执行也不是不行。
小结
- JS是单线程运行的,而浏览器是多线程运行的,浏览器应用Event Loop模型实现异步操作
- 一开始时,JS引擎把同步代码放入执行栈中执行,而异步代码交给相应的线程或API解决,等到有后果或达到触发条件了,就把注册的回调函数放入异步工作队列中期待主线程取出执行
- 随着ES6 Promise的原生实现和HTML5 MutationObserver 的退出,将异步工作细分为宏工作和微工作
- 宏工作的回调放入宏工作队列(能够有多个),微工作的回调放入微工作队列,并且每一次Event Loop循环中,都是先取出一个宏工作执行,而后取出所有微工作执行
- 一次Event Loop循环过程:整体代码(第一个宏工作)=>所有微工作=>UI渲染,接着进入下一轮循环。
Node.js中的Event Loop
后面咱们曾经讲到,node.js的Event Loop是基于libuv实现的,而node.js中的Event Loop和浏览器中的Event Loop运行机制有很大的不同,nodejs中 Event Loop 运行如下图:
node.js与浏览器中的Event Loop不同次要体现在:
- node.js中的Event Loop的单次循环是分阶段进行的。每个阶段运行完所有该阶段的回调函数或回调次数达到了次数限度,才会进入下一个阶段或指定的阶段,直到运行完最初一个阶段,进入下一个循环。
- 除了Poll阶段,node会在每个阶段,将该阶段对应的所有宏工作都顺次执行完,而后执行微工作队列中的所有工作。(留神:node.js 11.0及当前的版本中,Goole为了向浏览器靠齐,将这一行为改成与浏览器统一,即每个 Macrotask 执行完后,就去执行 Microtask 了)
node.js中,将Event Loop分为以下几个阶段:
Timers阶段
这个阶段执行setTimeout/setInterval的已到期的回调函数。执行程序:
- 所有setTimeout/setInterval的回调函数
- 所有process.nextTick的回调函数
- 所有微工作的回调函数
Pending callbacks阶段
即IO callbacks阶段。
依据libuv的文档,一些应该在上轮循环poll阶段执行的callback,因为某些起因不能执行,就会被提早到这一轮的循环的I/O callbacks阶段执行。换句话说这个阶段执行的callbacks是上轮残留的。
Idle/Prepare阶段
仅供外部应用(略)
Poll阶段
注:Node很多API都是基于事件订阅实现的,这些API的回调应该都在poll
阶段实现。
这个阶段的运行机制和其余阶段有所不同,也是最简单的阶段。体现如下:
- 当回调队列不为空时,会执行回调。与其余阶段不同的是,该阶段产生的微工作,不会等到所有宏工作的回调执行完之后再执行,而是执行完一个宏工作就执行所有微工作,即与浏览器的行为统一。
- 当回调队列为空的时候,这里又分两种状况:
1.如果有待执行的setImmediate
回调,那么事件循环间接完结poll
阶段进入check
阶段
2.如果没有待执行的setImmediate
设定回调,会查看有没有已到期的timers:
- 如果有,那么事件循环将回到
timers
阶段 - 如果没有,这个时候事件循环会阻塞在
poll
阶段期待回调被退出poll
队列
Check阶段
这个阶段执行setImmediate
的回调。执行程序:
所有setImmediate的回调函数
所有process.nextTick的回调函数
所有微工作的回调函数
Close callbacks阶段
执行一些敞开回调,如 socket.on('close', ...)
等
这6个阶段的运行如下图所示:
小结
- 每一个阶段都会有一个FIFO回调队列,都会尽可能的执行完以后阶段中所有的回调或达到了零碎相干限度,才会进入下一个阶段
- 除了Poll的各个阶段,执行程序都是:所有宏工作队列工作回调=>所有nextTick队列工作回调=>所有其余微工作回调
- Poll 阶段执行的微工作的机会和 Timers 阶段 & Check 阶段的机会不一样,前者是在每一个回调执行就会执行相应微工作,而后者是会在所有回调执行完之后,才对立执行相应微工作。
- node.js新版本中(v11.0及当前版本中),宏工作和微工作的执行程序和过程保持一致。
参考资料:
- 阮一峰《JavaScript 运行机制详解:再谈Event Loop》https://blog.csdn.net/qianyu6200430/article/details/108989045
- youth7《不要混同nodejs和浏览器中的event loop》https://cnodejs.org/topic/5a9108d78d6e16e56bb80882
- libuv文档:http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop
- liuxuan《带你彻底弄懂Event Loop》https://segmentfault.com/a/1190000016278115?utm_source=tag-newest
- 《[[译] 深刻了解 JavaScript 事件循环(二)— task and microtask](https://www.cnblogs.com/dong-...》https://www.cnblogs.com/dong-xu/p/7000139.html