概括来说是什么?
所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,本文章主要分析浏览器中的事件循环。
JS异步是怎么实现的?
我们都知道JS是单线程的,那单线程是怎么实现异步的呢?事实上所谓的"JS是单线程的"只是指JS的主运行线程只有一个,而不是整个运行环境都是单线程。JS的运行环境主要是浏览器,以大家都很熟悉的Chrome的内核为例,他不仅是多线程的,而且是多进程的:
进程和线程的关系,可以用个比喻来理解:
一个进程中有多个线程,就可以理解为一趟火车有多个车厢,火车是进程,每节车厢是线程;
所以容易理解到进程之间的通信成本更高,线程之间容易通信。
那浏览器的进程和线程如下图:
浏览器中新开的一个tab标签就是一个新的进程,崩溃了不影响其他的tab,不会直接导致整个浏览器崩溃。
浏览器的Event Loop
事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是事件队列。各个异步线程执行完后,通过事件触发线程将回调事件放到事件队列,主线程每次干完手上的活儿就来看看这个队列有没有新活儿,有的话就取出来执行。画成一个流程图就是这样:
流程讲解如下:
- 主线程每次执行时,先看看要执行的是同步任务,还是异步的API
- 同步任务就继续执行,一直执行完
- 遇到异步API就将它交给对应的异步线程,自己继续执行同步任务
- 异步线程执行异步API,执行完后,将异步回调事件放入事件队列上
- 主线程手上的同步任务干完后就来事件队列看看有没有任务
- 主线程发现事件队列有任务,就取出里面的任务执行
- 主线程不断循环上述流程
看一下同步中有耗时的任务时的情况如下:(会导致定时器不准)
const syncFunc = () => {
const time = new Date().getTime();
while(true) {
if(new Date().getTime() - time > 2000) {
break;
}
}
console.log(2);
}
console.log(1);
syncFunc();
console.log(3);
上述代码会先打印出1,然后调用syncFunc,syncFunc里面while循环会运行2秒,然后打印出2,最后打印出3。所以这里代码的执行顺序跟我们的书写顺序是一致,他是同步代码。
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);
执行结果如下:
通过结果可以看出,虽然我们先调用的asyncFunc,虽然asyncFunc写的是2秒后执行,但是syncFunc的执行时间太长,达到了5秒,asyncFunc虽然在2秒的时候就已经进入了事件队列,但是主线程一直在执行同步代码,一直没空,所以也要等到5秒后,同步代码执行完毕才有机会执行这个定时器回调。所以再次强调,写代码时一定不要长时间占用主线程。
事件队列里面的事件可以分两类:宏任务和微任务。
微任务拥有更高的优先级,当事件循环遍历队列时,先检查微任务队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。所以完整的流程图如下:
上图需要注意以下几点:
- 一个Event Loop可以有一个或多个事件队列,但是只有一个微任务队列。
- 微任务队列全部执行完会重新渲染一次
- 每个宏任务执行完都会重新渲染一次
- requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列
在本次事件循环中,产生的新的微任务和宏任务何时执行?
- 新的微任务:在当前微任务执行期间,如果产生新的微任务,这些新微任务会被添加到微任务队列,并会在当前微任务队列完成后立即执行。
- 新的宏任务:新的宏任务会被添加到任务队列的末尾,并会在下一轮事件循环中执行。
常见宏任务有:
script (可以理解为外层同步代码)
setTimeout/setInterval
setImmediate(Node.js)
I/O
UI事件
postMessage
常见微任务有:
Promise
process.nextTick(Node.js)
Object.observe
MutaionObserver