一、参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop
https://www.w3.org/TR/html5/webappapis.html#event-loops
https://juejin.im/post/5a6547d0f265da3e283a1df7#heading-4 (强烈推荐看完)
https://segmentfault.com/a/1190000006811224
https://blog.csdn.net/lin_credible/article/details/40143961
https://www.zhihu.com/question/36972010
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
二、let’s go!
(1).盗个图演示下栈和队列
上图很完美的诠释了栈和队列的区别。
- 图中栈呈竖直形状,且顶部开口,代表着栈后进先出的特点。why?只有顶部开口,那么只能从顶部进出啊!
- 图中队列水平横向放置,左侧是队列的出口,右侧是队列的入口,这不正好符合队列先进先出的特点吗?
(2).事件循环
实现方式示例:
123while (queue.waitForMessage()) {queue.processNextMessage()}什么是
event loop
?主线程从任务队列中依次取出“任务”去执行,当主线程在执行某个“任务”时,其他“任务”在任务队列里排好队等待,当主线程执行完一个“任务”后才会再度从任务队列里取“任务”,这样一直取–>执行–>取…直到任务队列中没有待执行的“任务”。
为什么需要
event loop
?- 3.1
js
运行环境的运行机制奠定了基础- 明确一点:
js
是单线程的!所有任务只能由一个线程(主线程)执行。这里就产生一个思考或者弊端:当主线程正在执行一个很耗时的比如从后台接收大量数据任务时,其他所有任务是不是只能等待主线程把它执行完才能被主线程“临幸”?这样肯定是不行的,一点也不优美。所以接下来就引入了同步异步的概念,而event loop
是实现异步的一种机制。
- 明确一点:
- 3.2 什么是同步异步?
- 同步任务其实就是主线程循规蹈矩按顺序依次执行并能立即得到执行结果的任务。所有同步任务在主线程上形成一个执行栈,栈就像一个竖直容器,顶部开口,任务一个一个的放进去,再一个一个的取出来执行,那么最先放进去的堆在最下面,也在最后被取出执行。关于执行栈可以看:https://www.cnblogs.com/MasterYao/p/5563725.html
- 异步任务其实可以理解为发出调用但是无法立即得到结果,需要进行其他操作才能得到预期结果。
- 当主线程执行某异步任务时,发出调用但是没有立即得到返回值,主线程也不会干等着,这时候异步任务会在“一旁”(下面会讲具体在哪)默默等待返回值,一旦有返回值便会把回调函数放入对应的任务队列(PS:一个事件循环可能会有多个任务队列,相同任务源的任务会放进同一个任务队列,不同任务源放入不同任务队列。可以看HTML5规范:https://www.w3.org/TR/html5/webappapis.html#event-loops)。
- 所以
event loop
机制最大的作用就是把异步任务的回调函数放入主线程执行。
整个运行机制可以看下图,或者本文开头的第四个链接
- 3.1
注意点:
- 主线程只有执行完正在执行的任务后才会去任务队列里面取新任务执行,如果没有新任务,那就等待。
setTimeout
经常造成一些困惑,其实setTimeout
并不是过了多久就执行,而是过了多久就放入任务队列中!setTimeout
可能会干扰任务队列中任务的顺序。- 同步任务不会放入任务队列里,直接压入主线程执行栈。
event loop
是实现异步的一种机制。
(3). 疑问(强烈推荐本文第三个链接):https://juejin.im/post/5a6547d0f265da3e283a1df7#heading-4
- 5.1 之前说到,主线程调用异步方法后不会停下来等待异步任务,主线程会先去执行其他异步任务调用,那么这时候刚刚被调用的异步任务去哪了?异步任务这时候不需要线程运行它吗?很多文章说挂起,然后有结果了放入任务队列等调用,但是,它怎么接收返回值并被放进任务队列的呢?
很多文章几乎都没有提过这个过程,直到我在segmentfault提问之后,才有大牛给了我本文第三个链接,再次感谢id为178096413的前辈。- 我看了文章才知道,浏览器是多进程的,自己太菜了。
- 前端最需要关注的是浏览器的渲染进程,渲染进程是多线程的!页面的渲染,JS的执行,事件的循环,都在这个进程内进行。
- 渲染进程包含的线程(部分重要举例):
- GUI渲染线程
- JS引擎线程
- 事件触发线程
- 控制事件循环
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 定时触发器线程
- 异步http请求线程
- 在
XMLHttpRequest
连接后时通过浏览器新开一个线程请求 - 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
- 在
看到这里,就能很容易解决我的疑问了。原来浏览器会有专门的线程去继续执行异步任务,当异步任务执行完且有回调函数时,异步线程才会把回调放进事件队列中(任务队列)。
- 5.2 事件队列或任务队列中的任务是什么?
可以简要的理解为异步任务的回调。 - 5.3
event loop
是基于主线程执行的吗?
事件循环机制是基于事件触发线程的。事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
(4). 初步总结
总算初步对event loop
有了较为清晰的认识,但还不够,接下来需要对事件循环进行深入理解:macrotask
与microtask
。
(5). 事件循环进阶:macrotask与microtask(这里基本上都是抄自第三个与第六个链接的,讲的很好,抄下来便于自己查看)
很经典的例子:
为什么呢?因为Promise
里有了一个一个新的概念:microtask
进一步,JS中分为两种任务类型:
macrotask
和microtask
,在ECMAScript中,microtask
称为jobs,macrotask
可称为task
macrotask
(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)- 每一个task会从头到尾将这个任务执行完毕,不会执行其它
- 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染(task->渲染->task->…)
microtask
(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务- 也就是说,在当前task任务后,下一个task之前,在渲染之前
- 所以它的响应速度相比
setTimeout
(setTimeout
是task)会更快,因为无需等渲染 - 也就是说,在某一个
macrotask
执行完后,就会将在它执行期间产生的所有microtask
都执行完毕(在渲染前)
分别很么样的场景会形成macrotask
和microtask
呢?
macrotask
:主代码块,setTimeout
,setInterval
,MessageChannel
等(可以看到,事件队列中的每一个事件都是一个macrotask)microtask
:Promise
,process.nextTick
,MutationObserver
等
补充:在node环境下,process.nextTick
的优先级高于Promise
,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue
部分,然后才会执行微任务中的Promise
部分。
再根据线程来理解下:macrotask
中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护microtask
中的所有微任务都是添加到微任务队列(Job Queues
)中,等待当前macrotask
执行完毕后执行,而这个队列由JS引擎线程维护
所以,总结下运行机制:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)