这是我参与「第四届青训营 」笔记创作活动的第3天
JS任务队列
首先我们需要明白以下几件事情:
- JS分为同步任务和异步任务
- 同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。 setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。
一、JS单线程、异步、同步概念
JS单线程操作DOM, 单线程即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给相应的异步模块去处理,主线程的效率大大提升,可以并行的去处理其他的操作。当异步处理完成,主线程空闲时,主线程读取相应的callback,进行后续的操作,最大程度的利用CPU。此时出现了同步执行和异步执行的概念,同步执行是主线程按照顺序,串行执行任务;异步执行就是cpu跳过等待,先处理后续的任务(CPU与网络模块、timer等并行进行任务)。由此产生了任务队列与事件循环,来协调主线程与异步模块之间的工作。
二、事件循环机制
如上图为事件循环示例图(或JS运行机制图),流程如下: step1:主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈; step2: 主线程遇到异步任务,指给对应的异步线程进行处理(WEB API); step3: 异步线程处理完毕(Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入任务队列; step4: 主线程执行完毕,查询任务队列,如果存在任务,则取出一个任务推入主线程处理(先进先出); step5: 重复执行step2、3、4;称为事件循环。 执行的大意: 同步环境执行(step1) -> 事件循环1(step4) -> 事件循环2(step4的重复)… 其中的异步线程有: a、类似onclick等,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列中; b、setTimeout等,由浏览器内核的Timer模块处理,时间到达时,回调函数添加到任务队列中; c、Ajax,由浏览器内核的Network模块处理,网络请求返回后,添加到任务队列中。
三、任务队列
如上示意图,任务队列存在多个,同一任务队列内,按队列顺序被主线程取走;
不同任务队列之间,存在着优先级,优先级高的优先获取(如用户I/O);
3.1、任务队列的类型
任务队列存在两种类型,一种为microtask queue,另一种为macrotask queue。 图中所列出的任务队列均为macrotask queue,而ES6 的 promise[ECMAScript标准]产生的任务队列为microtask queue。
一 宏任务(macrotask queue)
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->...
(macro)task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
二 微任务(microtask queue)
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境) 、await后面的语句(相当于Promise.then里面的语句)
3.2、两者的区别
microtask queue:唯一,整个事件循环当中,仅存在一个;执行为同步,同一个事件循环中的microtask会按队列顺序,串行执行完毕; macrotask queue:不唯一,存在一定的优先级(用户I/O部分优先级更高);异步执行,同一事件循环中,只执行一个。
3.3、更完整的事件循环流程
将microtask加入到JS运行机制流程中,则: step1、2、3同上, step4:主线程查询任务队列,执行microtask queue,将其按序执行,全部执行完毕; step5:主线程查询任务队列,执行macrotask queue,取队首任务执行,执行完毕; step6:重复step4、step5。 microtask queue中的所有callback处在同一个事件循环中,而macrotask queue中的callback有自己的事件循环。 简而言之:同步环境执行 -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个) -> 事件循环1(microtask queue的All)-> 事件循环2(macrotask queue中的一个)... 利用microtask queue可以形成一个同步执行的环境,但如果Microtask queue太长,将导致Macrotask任务长时间执行不了,最终导致用户I/O无响应等,所以使用需慎重。
Promise和async中的立即执行
我们知道Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await做了什么
从字面意思上看await就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
由于因为async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。所以对于本题中的
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
等价于
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
从线程角度来理解任务队列
- GUI渲染线程对html进行解析
- 当解析到script标签时,JS引擎解析拿到的js代码,解析script标签就是最先进入执行栈的共任务
- 同上,但是事件队列就被区分为宏任务事件队列task queue,微任务事件队列microtask queue
-
- 为dom元素,添加事件 => 通过事件触发线程,生成事件监听器(待确认)=> 监听器监听到了用户触发事件的行为之后,将回调函数,推入task queue中
- 解析到setTimeout或者setInterval定时器代码时 => 通过定时器线程,开启定时任务 => 定时器时间到达之后,会将回调函数推入task queue中
- 遇到ajax请求 => 通过异步http请求线程,发送http请求 => 服务端返回响应后,会将成功或者失败的回调函数推入task queue中
- 代码中使用了Promise或者async/await来处理异步 => 处理完成后 => 推入microtask queue中
- 当js执行线程中的代码执行完成之后,首先检查微任务队列头部是否有值,如果存在则将其推入到JS执行栈中执行,直到微任务队列头部为空。
- 如果宿主是浏览器,GUI渲染线程可能会重新渲染页面
- 然后检查宏任务队列头部是否有值,如果存在则将其推入到JS执行栈中执行,循环3456四个步骤,直到宏任务队列头部为空。
- 当JS执行线程中的执行栈为空时,事件轮询机制会一直重复4-6这个循环