摘要:本文经过结合官方文档MDN和其余博客深刻解析浏览器的事件循环机制,而NodeJS有另外一套事件循环机制,不在本文讨论范围中。process.nextTick和setImmediate是NodeJS的API,因此本文也不予讨论。
首先,先了解几个概念。javascript
Javascript是一门单线程语言。相信应该有很多朋友对于Javascript是单线程语言还有些疑问(题外话:以前在某次面试中遇到一个面试官,一来就是“咱们知道JS是一门多线程语言。。。”巴拉巴拉,当时就把我给愣住了。),不是有Web Worker能够建立多个线程吗?答案就是,Javascript是单线程的,可是他的运行环境不是单线程。要如何理解这句话,首先得从Javascript运行环境好比浏览器的多线程提及。 html
浏览器一般包含如下线程:java
Web Worker是浏览器为Javascript提供的一个能够在浏览器后台开启一个新的线程的API(相似上面说到浏览器的多个线程),使Javascript能够在浏览器环境中多线程运行,但这个多线程是指浏览器自己,是它在负责调度管理Javascript代码,让他们在恰当时机执行。因此Javascript自己是不支持多线程的。 ios
Javascript的异步过程一般是这样的:web
这个过程有个问题,异步任务各任务的执行时间过程长短不一样,执行完成的时间点也不一样,主线程如何调控异步任务呢?这就引入了消息队列。面试
栈:函数调用造成的一个由若干帧组成的栈。ajax
堆:对象被分配在堆中,堆是一个用来表示一大块(一般是非结构化的)内存区域。axios
消息队列:一个Javascript运行时包含了一个待处理消息的消息队列。每个消息都关联着一个用来处理这个消息的回调函数。在事件循环期间,运行时会从最早进入队列的消息开始处理,被处理的消息会被移出队列,并做为输入参数来调用与之关联的函数。而后事件循环在处理队列中的下一个消息。api
了解了上述要点,如今回到主题事件循环。那么Event loop究竟是什么呢?promise
Event loop是一个执行模型,在不一样的地方有不一样的实现。浏览器和NodeJS基于不一样的技术实现了各自的Event loop。
如今明白为何要把NodeJS排除在外了吧?一样网上不少Event loop的相关博文一来就是Javascript的Event loop,实际上说的都是浏览器的Event loop。
浏览器的Event loop是在Html5规范中定义的,大体总结以下:
一个事件循环里有不少个任务队列(task queues)来自不一样任务源,每个任务队列里的任务(task)都是严格按照先进先出的顺序执行的,可是不一样任务队列的任务执行顺序是不肯定的,浏览器会本身调度不一样任务队列。也有地方把task称之为macrotask(宏任务)。
规范中还提到了microtask(微任务)的概念,如下是规范阐述的进程模型:
执行进入microtask检查点时,用户代理会执行如下步骤:
由上可总结为:在事件循环中,用户代理会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),若是不为空则会一次性执行完全部microtask。而后再进入下一个循环去task队列中取下一个task执行。
来看一个例子:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
运行结果是:
script start script end promise1 promise2 setTimeout
那么问题来了,不是说每一个事件循环开始会从task队列取最早进入的task执行,而后再执行全部microtask吗?为何setTimeout是task却在Promise.then这个task的前面呢?反正我一开始是有这个疑惑的,不少文章都没有说清楚这个具体执行的顺序,大部分都是在描述规范的时候说的是“每一个事件循环开始会从task队列中取一个task执行,而后再执行全部microtask”,可是也有部分文章说的是“每一个事件循环开始都是先执行全部microtask”。通过本人多方查证,规范里的描述如上确实就是每一个事件循环都是先执行task,那为何上面例子里面体现出来的是先执行全部microtask呢?
script(总体代码)属于task。
来看一下上面例子的详细执行过程:
这样是否是清楚了?因此实际上一开始执行script代码的时候就已经开始事件循环了,这就解释了为何好像每次都是先执行全部的microtask。同时,这个例子中还引伸出一个要点:在执行microtask任务的时候,若是又产生了新的microtask,那么会继续添加到队列的末尾,且也会在这个事件循环周期执行,直到microtask队列为空为止。