1 在展开Event Loop话题之前,抛出一个问题,JS是单线程还是多线程?
Javascript是单线程脚本语言, 取决于它的实际用途,JS的主要用来操作DOM,与用户进行互动。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JS同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?于是很多人疑惑,既然JS是单线程,如果前一个任务执行时间很长,后续任务不是得一直等着?于是就有了异步事件的概念。
2 何为Js异步?
所谓的异步是指不会阻塞我们的主线程,常见的异步事件:setTimeout,setInterval,Promise等等。当浏览器执行到异步代码块,会交与Wep API去处理这些异步任务,把这些异步回调推入任务队列中(Event queue),当JS线程空闲时,执行任务队列中的任务。往复循环,这种机制称之为事件循环(Event Loop)。
3 MacroTask and MicroTask
第二点提到,Web API会处理异步事件,将异步回调推入任务队列,任务队列又分为宏任务队(MacroTask queue)和 微任务队列(microTask queue)。首先执行MacroTask队列中的一个宏任务,然后执行microTask队列中的所有微任务。接着开始下一次循环。
常见的MacroTask:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
常见的microTask:
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
4 浏览器中的事件循环
来一到面试题:
function f1() {
console.log(1);
setTimeout(() => {
console.log(2)
}, 0); // setTimeout1
}
new Promise(function(resolve, reject) {
console.log('promise1');
resolve();
}).then(function() {
console.log('3');
}); //promise1
setTimeout(() => {
console.log(4)
new Promise(function(resolve, reject) {
console.log('promise2');
resolve();
}).then(function() {
console.log(5);
});
}, 100); // setTimeout2
new Promise(function(resolve, reject) {
console.log('promise3');
resolve();
}).then(function() {
console.log(6);
}); //promise3
console.log(7);
f1();
正确输出为:promise1 promise3 7 1 3 6 2 4 promise2 5。如果你答对了,恭喜你,你对浏览器的事件循环已经基本掌握。没有答对的请继续往下看
来分析下上文那段代码:
1 首先执行全局JS代码,从上往下执行,首先输出promise1(注意,new promise为同步代码,promise的回调才是异步),promise3,7,1。到此为止应该没有问题吧?此阶段可以理解为第一个macroTask
2 setTimeout1(延迟为0,但并非为0,而是4ms),setTimeout2(延迟为100ms)被推入宏任务队列(因为setTimeout2的延迟很长,所以会在setTimeout1之后被推入宏任务队列,排在setTimeout1之后)。Promise1和promise3被推入微任务队列。
Note: 第二点和第一点是同时进行的
3 执行栈为空,从微任务队列拉取任务(先进先出原则)执行,直至微任务队列空,输出 3,6
4 执行栈为空,从宏任务队列拉取任务执行(一次循环只执行一个宏任务),执行setTimeout1,输出2
5 执行栈为空,检查微任务队列也为空,继续从宏任务队列拉取任务执行,执行setTimeout2,输出4,promise2,同时将promise2推入微任务队列
6 执行栈为空,从微任务队列拉取任务,执行promise2 callBack,输出5
上图描绘了Event Loop的运行机制模型:
1 执行全局Javascript同步代码:
首先Js Stack执行全局Javascript同步代码(后进先出),此阶段为第一个macroTask
将异步回调交与Wep Api, 注意交与Web API处理的异步任务顺序并不是任务队列中的排列顺序。上文提到的代码中, Web api会处理setTimeout2 和 setTimeout1, 由于setTimeout2设置的需要等待时间更长(设置100ms并不是100ms之后一定会执行,而是指100ms之后推入macroTask queue),所以setTimeout2回调会在100ms之后被推入macrotask queue,setTimeout1默认再4ms后背推入macrotask queue。promise被推入microTask queue
3 执行微任务
当Stack为空,从microTask queue中取队列首部任务,突入Stack执行,执行完后microTask queue长度减1,继续从microTask queue拉取任务执行,直至microTask queue为空
4 执行宏任务
microtask queue队列中的任务以全部执行完毕,并且stack为空,从macrotask queue中位于队首的任务,放入Stack中执行
5 循环3,4步骤
知识点:
- Stack后进先出,Queue先进先出
- 只有在执行栈为空的时候才会去从任务队列中拉取任务执行
- 有两个任务队列,宏任务队列(MacroTask queue)和微任务队列(MicroTask queue)
- MacroTask queue只执行一个task,执行完之后便会去执行microTask
- MicroTask queue依次执行,直至microTask queue 为空
- 将最先执行JS全局代码理解为执行第一个macro task, 那么每一次循环都是执行一个macro task, 执行整个micro task queue, 往复循环
以上就是浏览器的事件循环机制,字纯手打,图纯手绘,如有描述不清或描述有误的,欢迎留言探讨。如果本文有给你们帮助,请留个 star
附言:
Node端的事件循环机制与浏览器不同,差异很大,请关注后续推出的KNOW-FRONTEND:NodeJs Event Loop