你不知道的Event Loop

778 阅读5分钟

1 在展开Event Loop话题之前,抛出一个问题,JS是单线程还是多线程?

    Javascript是单线程脚本语言, 取决于它的实际用途,JS的主要用来操作DOM,与用户进行互动。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JS同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?于是很多人疑惑,既然JS是单线程,如果前一个任务执行时间很长,后续任务不是得一直等着?于是就有了异步事件的概念。

2 何为Js异步?

    所谓的异步是指不会阻塞我们的主线程,常见的异步事件:setTimeout,setInterval,Promise等等。当浏览器执行到异步代码块,会交与Wep API去处理这些异步任务,把这些异步回调推入任务队列中(Event queue),当JS线程空闲时,执行任务队列中的任务。往复循环,这种机制称之为事件循环(Event Loop)。

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

2  处理异步任务,将异步任务推入任务队列

   将异步回调交与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