看了阮老师的《再谈Event Loop》,有些地方还是不太清楚,所以又查了一些资料,资料链接在下面的参考中,总结一下。
线程
浏览器是多线程的,其中有
- 渲染引擎线程:负责页面的渲染
- JS引擎线程:负责JS的解析和执行
- 定时触发器线程:处理定时事件,比如setTimeout, setInterval
- 事件触发线程:处理DOM事件
- 异步http请求线程:处理http请求
渲染引擎就是如何渲染页面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不同的引擎对同一个样式的实现不一致,就导致了经常被人诟病的浏览器样式兼容性问题。
不同浏览器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。
其中渲染线程和JS引擎线程不能同时进行。平时我们说的js单线程只是JS引擎线程,而异步就是靠这些其他的线程来处理实现的,然后再通过事件循环、任务队列来传到js线程中。
事件循环
首先我们都知道js有同步和异步任务。
-
同步任务是指在js主线程上排队执行的任务,只有前一个任务执行完毕,后一个同步任务才能执行。
-
异步任务不在主线程执行,任务在主线程定义后到其他线程去执行,执行完毕后,会将结果放入任务队列,主线程的执行栈为空时,会读取任务队列,执行其中的任务。每个异步任务都和回调函数相关联。
JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。
异步任务可分为 task 和 microtask 两类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。
具体过程
- 执行完主执行线程中的任务。
- 取出Microtask Queue中任务执行直到清空。
- 取出Macrotask Queue(task)中一个任务执行。
- 取出Microtask Queue中任务执行直到清空。
- 重复3和4。
task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
在 Node 中,会优先清空 next tick queue,即通过process.nextTick 注册的函数,再清空 other queue,常见的如Promise;此外,timers(setTimeout/setInterval) 会优先于 setImmediate 执行,因为前者在 timer 阶段执行,后者在 check 阶段执行。
setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。
由于JS引擎线程空闲后,会先查看是否有事件可执行,接着再处理其他异步任务。所以如果click事件和setTimeout都该执行时,先执行click事件的回调函数。例如下面程序
setTimeout(function(){
console.log('timer');
}, 0)
function waitFiveSeconds(){
var now =