前端点滴(JS进阶)(二)----倾尽所有
1. JavaScript 单线程
JS的单线程的概念
JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征。
任务队列(消息队列)
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。JavaScript语言的设计者意识到这个问题,将所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
可以参考:https://blog.csdn.net/weixin_42614080/article/details/90346489
2. 同步任务说明
同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。如果在函数A返回的时候,调用者就能够得到预期的结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。
console.log('hello');//执行后,获得了返回结果
其次,如果函数是同步的,即使调用函数执行任务比较耗时,也会一致等待直到得到执行结果。
由上述说明中发现:
- JavaScript 单线程。
- 同步任务在主线程上排队执行。
- 同步任务执行顺序不可改变(随着JavaScript代码自上而下执行)。
问题的引出:同步任务在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。那么如果上一个任务解析时间很长,那么下面的代码就会被阻塞。
对于用户而言,阻塞就意味着"卡死",这样就导致了很差的用户体验,因此,说明了为什么JavaScript需要异步?那么js单线程又是如何实现异步的呢?
通过事件循环(event loop)实现 '异步’
(1)Event Loop 的引出
1. 什么是Event loop?
实际上event loop是一个执行模型,在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。
- 浏览器的Event Loop是在html5的规范中明确定义。
- NodeJS的Event Loop是基于libuv实现的。可以参考Node的官方文档以及libuv的官方文档。
特点:
- 一个事件循环(event loop)会有一个或多个任务队列(task queue)
task queue 就是 macrotask queue - 每一个 event loop 都有一个 microtask queue
- 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
2. Event Loop 基本原理
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会循环反复。
3. 浏览器中的宏队列和微队列
宏队列,macrotask,也叫tasks。或者叫Task queue (任务队列) 一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微队列,microtask,也叫jobs。实际上不是一个队列(html5的规范 Note4)。 另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:
- process.nextTick (Node独有)
- Promise
- Object.observe (废弃)
- MutationObserver(注:这里只针对浏览器和NodeJS)
4. 浏览器的Event Loop工作原理
执行JavaScript代码的具体流程:
- 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout和setlnterval;DOM事件;ES6中的Promise;Ajax异步请求等);
- 全局Script代码执行完毕后,调用栈(主线程)(执行栈)会清空;
- 从微队列microtask queue中取出位于队首的回调任务,放入调用栈(主线程)中执行,执行完后microtask queue长度减1;
- 继续取出位于队首的任务,放入调用栈(主线程)中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
- microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈也为空;
- 取出宏队列macrotask queue中位于队首的任务,放入调用栈(主线程)中执行;
- 执行完毕后,调用栈(主线程)为空;
- 重复第3-7个步骤,直至将宏任务(宏队列任务)与微任务(微队列任务)全部执行完。
- 。。。
口诀: 自上而下,先同后异,先微后宏,谁先谁上
-
自上而下:按照js代码自上而下执行,将任务分为宏队列任务以及微队列任务。
-
先同后异:遵照自上而下先执行同步任务,后执行异步任务(队列任务)。
-
先微后宏:在解决异步的过程中先解决微队列任务,再解决宏队列任务,注意: 要清楚队列执行顺序问题。总结如下:
-
谁先谁上:排在队首的先执行。
需要注意的是: 当前调用栈(执行栈)执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。
5. NodeJS中宏队列与微队列
NodeJS的Event Loop中,执行宏队列的回调任务有6个阶段,如下图:
各个阶段执行的任务如下:
- timers阶段:这个阶段执行setTimeout和setInterval预定的callback
- I/O callback阶段:执行除了close事件的callbacks、被timers设定的- callbacks、setImmediate()设定的callbacks这些之外的callbacks
- idle, prepare阶段:仅node内部使用
- poll阶段:获取新的I/O事件,适当的条件下node将阻塞在这里
- check阶段:执行setImmediate()设定的callbacks
- close callbacks阶段:执行socket.on(‘close’, …)这些callbacks
NodeJS中宏队列主要有4个
回调事件主要位于4个macrotask queue中:
- Timers Queue
- IO Queue
- Check Queue
- Close Callbacks Queue
这4个都属于宏队列,但是在浏览器中,可以认为只有一个宏队列,所有的macrotask都会被加到这一个宏队列中,但是在NodeJS中,不同的macrotask会被放置在不同的宏队列中。
NodeJS中微队列主要有2个:
- Next Tick Queue:是放置process.nextTick(callback)的回调任务的
- Other Micro Queue:放置其他microtask,比如Promise等
在浏览器中,也可以认为只有一个微队列,所有的microtask都会被加到这一个微队列中,但是在NodeJS中,不同的microtask会被放置在不同的微队列中。
6. NodeJS的Event Loop工作原理(轮询机制)
执行JavaScript代码的具体流程:
- 执行全局Script的同步代码
- 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
- 开始执行macrotask宏任务,共4(实际6)个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有父任务(不包括子任务),注意,这里是所有每个阶段宏任务队列的所有任务,在浏览器的Event Loop中是只取宏队列的第一个任务出来执行,每一个阶段的macrotask任务执行完毕后,开始执行微任务。
- Timer Queue -> 步骤2 -> IO Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue …
口诀: 自上而下,先同后异,分队列,按步骤,父执行,子等待
- 分队列:将每一个任务分清,例如setTimeout() 属于timer Queue,process.nextTick() 属于 Next Tick Queue…
- 按步骤:按照Timer Queue -> 步骤2 -> IO Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue …
- 父执行,子等待:若父子同属一个任务队列,则父执行,子不执行(等待)
(2)ES6 Promise
Promise 对象用于处理异步请求,保存一个异步操作最终完成(或失败)的结果,用于解决异步问题的方法之一。
语法:
new Promise(
/* executor(执行器) */
function(resolve, reject) {...}
);
/*
promise:承诺,约定
resolve:解决,决定,下决心
reject:拒绝,驳回,抵制
*/
参数:
promise 构造函数接受一个 executor 函数作为参数,该函数的两个参数分别是 resolve 和 reject,实际上它们是两个函数
注意的是:(executor 函数 在 Promise 构造函数返回新对象之前被调用)说明 executor 函数属于同步任务。
存在情况:
-
resolve 函数被调用时,将 promise 对象从 “未完成” 变为 “成功” (即 pending --> fulfilled)
-
reject 函数被调用时,将 promise 对象从 “未完成” 变为 “失败” (即 pending --> rejected)
描述:
promise 对象是一个代理对象(代理一个值),被代理的值在 promise 对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法,使得异步方法可以像同步方法那样返回值,但不是立即返回最终执行结果,而是一个能代表未来出现的结果的 promise 对象。
上面提到过,一个 promise 对象有三个状态:
-
pending:初始状态,不是成功或失败状态
-
fulfilled:成功完成
-
rejected:失败
pending 状态可能触发 fulfilled 状态并传递一个值给相应的状态处理方法,也可能触发 rejected 状态并传递失败信息。当其中任一种情况发生时,promise 对象的 then 方法绑定的处理方法就会被调用。
(then 方法包含两个参数:onfulfilled 和 onrejected,也都是函数。当 promise 对象的状态为 fulfilled 时(也就是resolve 函数被调用时)调用,调用 then 的 onfulfilled 方法;反之,调用 onrejected 方法。所以异步操作的完成和绑定处理方法之间不存在竞争)。
以下是示意图:
then 的基本使用:
/* 写法一 */
var p = new Promise(function(resolve,reject){
console.log(1)
resolve("1");
});
p.then(function(res){
console.log(res);
}) //=> 1 "1"
/* 写法二 */
new Promise(function(resolve,reject){
resolve("1");
}).then(function(res){
console.log(res);
}).then(function(){
console.log(1);
}) //=> "1" 1
/* 写法三 */
Promise.resolve("1").then(function(res){
console.log(res)
}) //=> "1"
then 的多重嵌套:
实例:
new Promise(function(resolve){
resolve(1);
})
.then(function(value){
return new Promise(function(resolve){
resolve(2);
})
.then(function(value){
return value;
})
.then(function(value){
return value; //如果此处没有return,则输出undefined
})
})
.then(function(value){
console.log(value)
}) //=> 输出2,原因: return new Promise加上then的return值
(3)实例题(重点)(浏览器)
接下来就使使口诀看看是否奏效:
浏览器Event Loop口诀: 自上而下,先同后异,先微后宏,谁先谁上
/* 实例一 */
setTimeout(function(){console.log(1)},0);
console.log(2)
//=> 2 1 (先同后异)
/* 实例二 */
setTimeout(function () {
console.log(3);
}, 0);
Promise.resolve().then(function () {
console.log(2);
});
console.log(1);
//=> 1 2 3 (先同后异,先微后宏)
/* 实例三 */
setTimeout(function(){console.log(1)},0);
new Promise(function(resolve,reject){
console.log(2);
resolve();
}).then(function(){
console.log(3)
}).then(function(){
console.log(4)
});
process.nextTick(function(){console.log(5)});
console.log(6);
//=> 2 6 5 3 4 1 (先同后异,先微后宏)
分析Queue的执行顺序:nextTick Queue——>promise Queue
/* 实例四 */
setTimeout(function(){console.log(1)},0);
new Promise(function(resolve,reject){
console.log(2);
setTimeout(function(){resolve()},0)
}).then(function(){
console.log(3);
}).then(function(){
console.log(4);
});
process.nextTick(function(){console.log(5)});
console.log(6);
//=> 2 6 5 1 3 4 (自上而下,先同后异,先微后宏,谁先谁上)
解释:promise的构造中,没有同步的resolve,因此promise.then在当前的执行队列中是不存在的,只有promise从pending转移到resolve,才会有then方法,而这个resolve是在一个setTimout时间中完成的,但是在其上还有一个setTimeout(自上而下,谁先谁上),因此先输出1,后输出3,4。
/* 实例五(比较经典) */
Promise.resolve().then(()=>{
console.log('1')
setTimeout(()=>{ //2号setTimeout
console.log('2')
},0)
})
setTimeout(()=>{ //1号setTimeout
console.log('3')
Promise.resolve().then(()=>{
console.log('4')
})
},0)
//=> '1' '3' '4' '2' (自上而下,先微后宏,谁先谁上)
解释说明:
Step1:自上而下,promise 进入微队列,setTimeout进入宏队列。
- mi:[Promise]
- ma:[setTimeout()1]
Step2:先微后宏,先解决微队列任务,解决过程中又发现了宏任务(宏队列任务)。先输出 ‘1’,再将宏队列任务放入宏队列中,清空微队列。
- mi:[]
- ma:[setTimeout()1,setTimeout()2]
Step3:先微后宏,微队列清空了接下来就是宏队列。谁先谁上,所以先解决setTimeout()1。在解决setTimeout()1时又发现存在微队列任务。所以先输出 ‘3’ ,将setTimeout()1中的Promise放入微队列中。
- mi:[Promise]
- ma:[setTimeout()2]
Step4:先微后宏。先输出 ‘4’ ,后输出 ‘2’ 。队列清空。
- mi:[]
- ma:[]
所以结果: ‘1’ ‘3’ ‘4’ ‘2’
/* 实例六(比较经典) */
setTimeout(function(){console.log(4)},0);
new Promise(function(resolve){
console.log(1)
for( var i=0 ; i<10000 ; i++ ){
i==9999 && resolve()
}
console.log(2)
}).then(function(){
console.log(5)
});
console.log(3);
//=> 1 2 3 5 4 (自上而下,先同后异,先微后宏)
解释说明:
Step1:自上而下,先同后异,Promise中 executor 函数属于同步任务,所以自上而下执行。注意:输出1以后接着跑1000次循环才调用resolve方法,其实resolve()的意思是把 Promise对象实例的状态从pending变成 fulfilled(即成功),成功的回调就是对应的then方法。仅仅如此而已,所以resolve() 后面的 console.log(2) 会先执行。输出 1 2 3,promise.then进入微队列,setTimeout() 进入宏队列,等待被调用。
- mi:[Promise.then]
- ma:[setTimeout()]
Step2:先微后宏,输出 5 再输出 4;情空队列。
- mi:[]
- ma:[]
所以结果: 1 2 3 5 4
/* 实例七 */
console.log(1);
setTimeout(() => { //1
console.log(2);
Promise.resolve().then(() => { //1
console.log(3)
});
});
new Promise((resolve, reject) => { //2
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
})
setTimeout(() => { //2
console.log(6);
})
console.log(7);
// => 1 4 7 5 2 3 6 (自上而下,先同后异,先微后宏,谁先谁上)
解释说明:
Step1:自上而下,先同后异。输出1 4 7,Promise2进入微队列,setTimeout()1,setTimeout()2进入宏队列。
- mi:[Promise2]
- ma:[setTimeout()1,setTimeout()2]
Step2:先微后宏。先解决微队列任务因为resolve(5)所以输出 5,清空微队列。
- mi:[]
- ma:[setTimeout()1,setTimeout()2]
Step3:先微后宏,谁先谁上。后解决宏队列任务所以先解setTimeout()1,但是在此过程中发现存在微队列任务。所以先输出 2 ,Promise1进入微队列。
- mi:[Promise1]
- ma:[setTimeout()2]
Step4:先微后宏。先输出 3 ,后输出 6。清空队列。
- mi:[]
- ma:[]
所以结果: 1 4 7 5 2 3 6
/* 实例八 */
console.log(1);
setTimeout(() => { //1
console.log(2);
Promise.resolve().then(() => { //1
console.log(3)
});
});
new Promise((resolve, reject) => { //2
console.log(4)
resolve(5)
}).then((data) => {
console.log(data);
Promise.resolve().then(() => { //3
console.log(6)
}).then(() => {
console.log(7)
setTimeout(() => { //2
console.log(8)
}, 0);
setTimeout(() => { //3
console.log(9)
}, 0);
});
})
setTimeout(() => { //4
console.log(10);
})
console.log(11);
//=> 1 4 11 5 6 7 2 3 10 8 9 (自上而下,先同后异,先微后宏,谁先谁上)
再来分析一波:
Step1:自上而下,先同后异。输出 1 4 11,Promise2进入微队列,setTimeout()1,setTimeout()4进入宏队列。等待被调用。
- mi:[Promise2]
- ma:[setTimeout()1,setTimeout()4]
Step2:先微后宏。先解决微队列任务,此时注意遇到执行microtask queue中任务的时候,如果又产生了microtask,那么会继续添加到队列的末尾,也会在这个周期执行,直到microtask queue为空停止。又遇到宏队列任务。所以输出5 6 7,setTimeout()2,setTimeout()3自上而下进入红队列中,清空微队列。
- mi:[]
- ma:[setTimeout()1,setTimeout()4,setTimeout()2,setTimeout()3]
Step3:先微后宏,谁先谁上。后解决宏队列任务。解决setTimeout()1发现存在微队列任务。所以输出 2 ,Promise1进入微队列。
- mi:[Promise1]
- ma:[setTimeout()4,setTimeout()2,setTimeout()3]
Step4:先微后宏,谁先谁上。先解决微队列任务输出 3,后解决宏队列任务,谁先谁上,依次输出 10 8 9
所以结果:1 4 11 5 6 7 2 3 10 8 9
/* 执行下面这段代码,执行后,在 5s 内点击两下,过一段时间(>5s)后,再点击两下,整个过程的输出结果是什么? */
setTimeout(function(){
for(var i = 0; i < 100000000; i++){}
console.log('timer a');
}, 0)
for(var j = 0; j < 5; j++){
console.log(j);
}
setTimeout(function(){
console.log('timer b');
}, 0)
function waitFiveSeconds(){
var now = (new Date()).getTime();
while(((new Date()).getTime() - now) < 5000){}
console.log('finished waiting');
}
document.addEventListener('click', function(){
console.log('click');
})
console.log('click begin');
waitFiveSeconds();
//=> 0 1 2 3 4 'click begin' 'finished waiting' 'click'(<5s) 'timer a' 'timer b' 'click'(>5s)
解释说明:
同步任务结束后,js引擎线程空闲后会查看是否有事件可执行,接着在处理其他异步任务。所以5s内对页面进行点击,同步任务结束后执行,再处理异步任务。5s后对页面进行点击,js引擎线程空闲状态(再无任务可执行)再执行事件。
(4)实例题(重点)(NodeJs)
提个醒:
Timer Queue -> 步骤2 -> IO Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue …
NodeJsEvent Loop口诀: 自上而下,先同后异,分队列,按步骤,父执行,子等待
/* 实例一 */
console.log('start');
setTimeout(() => { // 父callback1
console.log(1);
setTimeout(() => { // 子callback2
console.log(2);
}, 0);
setImmediate(() => { // 子callback3
console.log();
})
process.nextTick(() => { // 子callback4
console.log(4);
})
}, 0);
setImmediate(() => { // 父callback5
console.log(5);
process.nextTick(() => { // 子callback6
console.log(6);
})
})
setTimeout(() => { // 父callback7
console.log(7);
process.nextTick(() => { // 子callback8
console.log(8);
})
}, 0);
process.nextTick(() => { // 父callback9
console.log(9);
})
console.log('end');
//=> "start" "end" 9 1 7 4 8 5 3 6 2
解释说明:
Step1:自上而下,先同后异。输出"start",“end”。
Step2:分队列。
- timer Queue:[父callback1,父callback7]
- check Queue:[父callback5]
- next Tick Queue:[父callback9]
Step3:按步骤。NodeJs Event Loop的开始是微任务。所以开始依次执行微任务Next Tick Queue中的全部回调任务。此时next Tick Queue中只有一个父callback9,将其取出放入调用栈中执行,打印9。清空next Tick Queue。
- timer Queue:[父callback1,父callback7]
- check Queue:[父callback5]
- next Tick Queue:[]
Step4:按步骤。执行第1个阶段timer Queue中的所有任务,先取出父callback1执行,打印1,父callback1函数继续向下,依次把子callback2放入timer Queue中,把子callback3放入check Queue中,把子callback4放入next Tick Queue中,然后父callback1执行完毕。再取出timer Queue中此时排在首位的父callback7执行,打印7,把子callback8放入next Tick Queue中,执行完毕。注意: 若父子同属一个任务队列,则父执行,子不执行(等待)!!!比如: 父callback1与子callback2。
- timer Queue:[子callback2]
- check Queue:[父callback5,子callback3]
- next Tick Queue:[子callback4,子callback8]
Step5:按步骤。每阶段的宏任务队列执行完毕后,都会开始执行微任务。
所以输出 4 8。清空微队列任务。
- timer Queue:[子callback2]
- check Queue:[父callback5,子callback3]
- next Tick Queue:[]
Step6:按步骤。下一阶段IO Queue 跳过。到Check Queue阶段,执行Check Queue中的所有任务。取出父callback5执行,打印5,把子callback6放入next Tick Queue中,执行子callback3,打印3。清空check Queue队列。存在疑问?为什么父callback5执行,子callback3也执行?不是说父执行,子不执行吗?原因:子callback3的父是谁?与父callback5有关系吗?半毛钱关系都没有。
- timer Queue:[子callback2]
- check Queue:[]
- next Tick Queue:[子callback6]
Step6:按步骤。每阶段的宏任务队列执行完毕后,都会开始执行微任务。所以先输出 6 ,最后输出 2。清空队列。
- timer Queue:[]
- check Queue:[]
- next Tick Queue:[]
所以结果:“start” “end” 9 1 7 4 8 5 3 6 2
/* 实例二 */
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
process.nextTick(function() {
console.log('6');
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
//=> 1 7 6 8 2 4 9 11 3 10 5 12 (比较简单)
3. 异步任务说明
异步:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。如果在函数A返回的时候,调用者还不能马上得到预期的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的 。(比如setTimeout和setlnterval;DOM事件;ES6中的Promise;Ajax异步请求等)
参考文章:
https://segmentfault.com/a/1190000018181334
总结
- JavaScript单线程。将所有任务分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
- 浏览器的Event Loop和NodeJS的Event Loop是不同的,实现机制也不一样,不要混为一谈。
- 浏览器环境下的Event Loop
实现机制:自上而下,先同后异,先微后宏,谁先谁上 - NodeJs环境下的Event Loop
实现机制:自上而下,先同后异,分队列,按步骤,父执行,子等待
- 浏览器可以理解成只有1个宏任务队列和1个微任务队列,先执行全局Script代码,执行完同步代码调用栈清空后,从微任务队列中依次取出所有的任务放入调用栈执行,微任务队列清空后,从宏任务队列中只取位于队首的任务放入调用栈执行,注意这里和Node的区别,只取一个,然后继续执行微队列中的所有任务,再去宏队列取一个,以此构成事件循环。
- NodeJS可以理解成有4个宏任务队列和2个微任务队列,但是执行宏任务时有6个阶段。先执行全局Script代码,执行完同步代码调用栈清空后,先从微任务队列Next Tick Queue中依次取出所有的任务放入调用栈中执行,再从微任务队列Other Microtask Queue中依次取出所有的任务放入调用栈中执行。然后开始宏任务的6个阶段,每个阶段都将该宏任务队列中的所有任务都取出来执行(注意,这里和浏览器不一样,浏览器只取一个),每个宏任务阶段执行完毕后,开始执行微任务,再开始执行下一阶段宏任务,以此构成事件循环。
- MacroTask包括: setTimeout、setInterval、 setImmediate(Node)、requestAnimation(浏览器)、IO、UI rendering
Microtask包括: process.nextTick(Node)、Promise、Object.observe、MutationObserver