所以JS异步的实现靠的就是浏览器的多线程,当他遇到异步API时,就将这个任务交给对应的线程,当这个异步API满足回调条件时,对应的线程又通过事件触发线程将这个事件放入任务队列,然后主线程从任务队列取出事件继续执行。这个流程我们多次提到了任务队列,这其实就是Event Loop,下面我们详细来讲解下。
Event Loop所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,我们会分开来讲。
浏览器的Event Loop事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是事件队列。各个异步线程执行完后,通过事件触发线程将回调事件放到事件队列,主线程每次干完手上的活儿就来看看这个队列有没有新活儿,有的话就取出来执行。画成一个流程图就是这样:
流程讲解如下:
主线程每次执行时,先看看要执行的是同步任务,还是异步的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);执行结果如下:
通过结果可以看出,虽然我们先调用的asyncFunc,虽然asyncFunc写的是2秒后执行,但是syncFunc的执行时间太长,达到了5秒,asyncFunc虽然在2秒的时候就已经进入了事件队列,但是主线程一直在执行同步代码,一直没空,所以也要等到5秒后,同步代码执行完毕才有机会执行这个定时器回调。所以再次强调,写代码时一定不要长时间占用主线程。
引入微任务前面的流程图我为了便于理解,简化了事件队列,其实事件队列里面的事件还可以分两类:宏任务和微任务。微任务拥有更高的优先级,当事件循环遍历队列时,先检查微任务队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。所以完整的流程图如下: