看了阮老师的《再谈Event Loop》,有些地方还是不太清楚,所以又查了一些资料,资料链接在下面的参考中,总结一下。
浏览器是多线程的,其中有
渲染引擎就是如何渲染页面,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 将它们依次压入执行栈中执行。
具体过程
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 = (new Date()).getTime();
while(((new Date()).getTime() - now) < 5000){}
console.log('finished waiting');
}
document.addEventListener('click', function(){
console.log('click');
})
console.log('click begin');
waitFiveSeconds();
程序开始后,在5s内点击一次触发click事件,输出结果为
click begin
finished waiting
click
timer
另一个例子
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
console.log('then1')
})
console.log('script end');
script start
promise1
script end
then1
timeout1
timeout2
再两个例子
// example 1
new Promise(resolve => {
resolve(1)
Promise.resolve().then(() => console.log(42)) // then1
console.log(4)
}).then(t => console.log(t)) // then2
console.log(3)
// 输出结果依此为:4, 3, 42, 1
//example 2
let thenable = {
then: (resolve, reject) => {
resolve(42)
}
}
new Promise(resolve => {
resolve(1)
Promise.resolve(thenable).then((t) => console.log(t)) // then1
console.log(4)
}).then(t => console.log(t)) // then2
console.log(3)
// 输出结果依此为:4, 3, 1, 42
在Promise.resolve()的四种参数情况中的“无参数”,直接返回一个resolved状态的 Promise 对象,此时resolve的 Promise 对象是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。执行到‘Promise.resolve().then(() => console.log(42))’的回调函数then()被立即放入Microtask Queue中记为then1,然后继续执行外层回调函数then(),放入Microtask Queue中记为then2,所以then1优先于then2执行,先输出42后输出1
前面说的都是浏览器中的事件循环
node中的有些不同
process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。
在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:
按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:
https://juejin.im/post/5a6ad46ef265da3e513352c8
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://github.com/dwqs/blog/issues/61
https://juejin.im/post/5aa5dcabf265da239c7afe1e