setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop (2)

所以JS异步的实现靠的就是浏览器的多线程,当他遇到异步API时,就将这个任务交给对应的线程,当这个异步API满足回调条件时,对应的线程又通过事件触发线程将这个事件放入任务队列,然后主线程从任务队列取出事件继续执行。这个流程我们多次提到了任务队列,这其实就是Event Loop,下面我们详细来讲解下。

Event Loop

所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,我们会分开来讲。

浏览器的Event Loop

事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是事件队列。各个异步线程执行完后,通过事件触发线程将回调事件放到事件队列,主线程每次干完手上的活儿就来看看这个队列有没有新活儿,有的话就取出来执行。画成一个流程图就是这样:

image-20200320161732238

流程讲解如下:

主线程每次执行时,先看看要执行的是同步任务,还是异步的API

同步任务就继续执行,一直执行完

遇到异步API就将它交给对应的异步线程,自己继续执行同步任务

异步线程执行异步API,执行完后,将异步回调事件放入事件队列上

主线程手上的同步任务干完后就来事件队列看看有没有任务

主线程发现事件队列有任务,就取出里面的任务执行

主线程不断循环上述流程

定时器不准

Event Loop的这个流程里面其实还是隐藏了一些坑的,最典型的问题就是总是先执行同步任务,然后再执行事件队列里面的回调。这个特性就直接影响了定时器的执行,我们想想我们开始那个2秒定时器的执行流程:

主线程执行同步代码

遇到setTimeout,将它交给定时器线程

定时器线程开始计时,2秒到了通知事件触发线程

事件触发线程将定时器回调放入事件队列,异步流程到此结束

主线程如果有空,将定时器回调拿出来执行,如果没空这个回调就一直放在队列里。

上述流程我们可以看出,如果主线程长时间被阻塞,定时器回调就没机会执行,即使执行了,那时间也不准了,我们将开头那两个例子结合起来就可以看出这个效果:

const syncFunc = (startTime) => { const time = new Date().getTime(); while(true) { if(new Date().getTime() - time > 5000) { break; } } const offset = new Date().getTime() - startTime; console.log(`syncFunc run, time offset: ${offset}`); } const asyncFunc = (startTime) => { setTimeout(() => { const offset = new Date().getTime() - startTime; console.log(`asyncFunc run, time offset: ${offset}`); }, 2000); } const startTime = new Date().getTime(); asyncFunc(startTime); syncFunc(startTime);

执行结果如下:

image-20200320163640760

通过结果可以看出,虽然我们先调用的asyncFunc,虽然asyncFunc写的是2秒后执行,但是syncFunc的执行时间太长,达到了5秒,asyncFunc虽然在2秒的时候就已经进入了事件队列,但是主线程一直在执行同步代码,一直没空,所以也要等到5秒后,同步代码执行完毕才有机会执行这个定时器回调。所以再次强调,写代码时一定不要长时间占用主线程

引入微任务

前面的流程图我为了便于理解,简化了事件队列,其实事件队列里面的事件还可以分两类:宏任务和微任务。微任务拥有更高的优先级,当事件循环遍历队列时,先检查微任务队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。所以完整的流程图如下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppszw.html