一、事件循环机制
- JS引擎逐行扫描JS代码,遇到
同步任务
会将其添加到JS引擎线程的执行栈
中。 - 遇到异步任务,交由浏览器渲染进程中的相应线程(比如
setTimeout
交给定时器线程
,http
请求交给异步网络请求线程
)管理,当异步任务有了运行结果,事件触发线程就会将异步任务的事件回调依次放进任务队列
的末尾。该线程维护一个微任务队列
和一个宏任务队列
。 执行栈
中的同步任务执行完之后,JS引擎会从宏任务队列队首取一个宏任务,和微任务队列中到执行栈里,直到微任务队列为空。
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 微任务执行完成,GUI线程开始渲染工作
- 渲染完毕后,JS线程继续接管,开始下一个宏任务,依次循环
一旦主线程执行栈
中的所有同步任务执行完毕,主线程就会读取任务队列
中的任务添加到执行栈
中 ,继续开始执行,执行完毕后,就会再去任务队列中读取,这种循环往复的执行机制就是事件循环
。
示例:
let setTimeoutCallBack = function() {
console.log('定时器回调');
};
let httpCallback = function() {
console.log('http请求回调');
}
// 同步任务
console.log('同步任务1');
// 异步定时任务
setTimeout(setTimeoutCallBack, 1000);
// 异步http请求任务
ajax.get('/info', httpCallback);
// 同步任务
console.log('同步任务2');
代码执行过程:
- 执行
console.log('同步任务1')
。 - 执行
setTimeout(setTimeoutCallBack, 1000);
,会将该异步任务移交给定时器线程,定时器线程在1s 后将setTimeoutCallBack
这个回调交给事件触发线程处理,事件触发线程收到回调后把它加入到事件触发线程所管理的任务队列中等待执行。 - 执行
ajax.get('/info', httpCallback);
,会将该异步任务移交给异步http请求线程,http请求线程在请求结束后将httpCallback
这个回调交由事件触发线程处理,事件触发线程收到 回调后把它加入到事件触发线程所管理的事件队列中等待执行。 - 执行
console.log('同步任务2')
。 - 至此执行栈中的任务执行完毕,JS引擎线程已经空闲,开始向事件触发线程发起询问,询问事件触发线程的任务队列中是否有需要执行的回调函数,如果有将任务队列中的回调加入执行栈中,开始执行回调;如果任务队列中没有回调,JS引擎线程会一直发起询问,直到有为止。
二、宏任务 & 微任务
1.宏任务(macrotask|task)
本质: 宿主(Node或浏览器)发起的任务。
主要包含:
整体代码script
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
(Web Worker)setImmediate
(setTimeout(fn, 0))
2.微任务(microtask)
本质:Javascript 引擎发起的任务。
主要包含:
process.nextTick(Node.js)
Promise.then
catch
、finally
MutationObserver
(DOM变化观察器)
任务队列中分为宏任务队列
和微任务队列
,每执行一次任务都可能注册新的宏任务或微任务到相应的任务队列中,遵循每执行一个宏任务,就会清空一次任务队列中的所有微任务
这一循环规则。
大致过程如下:
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 微任务执行完成,GUI线程开始渲染工作
- 渲染完毕后,JS线程继续接管,开始下一个宏任务,依次循环
三、示例代码
示例1
setTimeout(function(){
console.log('setTimeout')
});
new Promise(function(resolve){
console.log('pro');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('then')
});
console.log('end');
// 打印结果
pro
end
then
setTimeout
代码执行过程:
- 整段代码作为宏任务执行,遇到
setTimeout()
分配到宏任务队列中; - 遇到
new Promise
,其中的代码为同步代码,立即执行。打印pro
; - 将
then
的回调分配到微任务队列中; - 遇到同步代码
console.log
,打印end
; - 至此主代码宏任务执行结束;
- 将微任务队列中的代码提取到主线程开始执行,打印
then
; - 微任务队列中执行完毕后,读取宏任务队列中的下一个宏任务开始执行,打印
setTimeout
。
示例2
微任务和宏任务嵌套创建:
console.log('1');
// 记作 set1
setTimeout(function () {
console.log('2');
// set4
setTimeout(function() {
console.log('3');
});
// pro2
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
// 记作 pro1
new Promise(function (resolve) {
console.log('6');
resolve();
}).then(function () {
console.log('7');
// set3
setTimeout(function() {
console.log('8');
});
})
// 记作 set2
setTimeout(function () {
console.log('9');
// 记作 pro3
new Promise(function (resolve) {
console.log('10');
resolve();
}).then(function () {
console.log('11');
})
})
// 代码打印结果
1
6
7
2
4
5
9
10
11
8
3
代码执行过程:
整体代码作为第一个宏任务进入执行栈中开始执行。打印
1
;主线程 Event Queue 执行栈 宏任务队列 微任务队列 整体js代码 遇到
set1
, 将set1回调
分发到宏任务队列中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 整体js代码 set1回调 遇到
pro1
,打印6
,将pro1的then回调
分发至微任务队列。主线程 Event Queue 执行栈 宏任务队列 微任务队列 整体js代码 set1回调 pro1的then回调 遇到
set1
, 将set2回调
分发到宏任务队列中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 整体js代码 set1回调 pro1的then回调 set2回调 整体js代码
宏任务至此执行完毕,js引擎读取微任务队列
到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 pro1的then回调 set1回调 set2回调 将
pro1的then回调
读取到执行栈中,打印7
,将set3的回调
分发至宏任务队列。主线程 Event Queue 执行栈 宏任务队列 微任务队列 pro1的then回调 set1回调 set2回调 set3回调 pro1的then回调
执行完毕,且微任务队列中已经无任务,读取宏任务队列,将set1回调
宏任务添加到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 set1回调 set2回调 set3回调 打印
2
,将set4回调
分发至宏任务队列;遇到pro2
,打印4
,将pro2的then回调
分发至微任务队列。主线程 Event Queue 执行栈 宏任务队列 微任务队列 set1回调 set2回调 pro2的then回调 set3回调 set4回调 set1回调
宏任务执行完毕,读取微任务队列到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 pro2的then回调 set2回调 set3回调 set4回调 执行
pro2的then回调
,打印5
。执行完毕后,读取下一个宏任务set2回调
到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 set2回调 set3回调 set4回调 打印
9
,执行pro3
,打印10
,将pro3的then回调
分发到微任务队列中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 set2回调 set3回调 pro3的then回调 set4回调 set2回调
执行完毕,将微任务队列
中的pro3的then回调
添加到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 pro3的then回调 set3回调 set4回调 打印
11
,pro3的then回调
执行完毕,将宏任务队列中的set3回调
添加到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 set3回调 set4回调 打印
8
,set3回调
执行完毕,将宏任务队列中的set4回调
添加到执行栈中。主线程 Event Queue 执行栈 宏任务队列 微任务队列 set4回调 打印
3
。
示例3
async function async1() {
console.log(1);
const result = await async2();
console.log(3);
}
async function async2() {
console.log(2);
}
// 记作pro
Promise.resolve().then(() => {
console.log(4);
});
// 记作set
setTimeout(() => {
console.log(5);
});
async1();
console.log(6);
// 打印结果
1
2
6
4
3
5
代码执行流程:
- 执行
pro
,将pro的then回调
分发到微任务队列中; - 执行
set
,将set回调
添加到宏任务队列; - 执行
async1()
,打印1
; - 执行
const result = await async2()
,打印2
;同时将console.log(3)
分发到微任务队列; - 继续执行主线程中的代码,打印
6
; - 执行微任务队列中的回调,依次打印
4
、3
; - 执行宏任务队列中的回调,打印
5
。
示例4
async function t1 () {
console.log(1)
// 记作pro2
new Promise(function(resolve) {
console.log('promise3')
resolve();
}).then( function () {
console.log('promise4')
})
// 记作pro3
await new Promise(function(resolve) {
console.log('b')
resolve();
}).then( function() {
console.log('t1p')
})
// 记作await pro3
console.log(2)
// 记作pro5
new Promise(function(resolve) {
console.log('promise5')
resolve();
}).then(function() {
console.log('promise6')
})
}
// 记作set
setTimeout(function() {
console.log('setTimeout')
}, 0)
async function t2() {
console.log(3);
// 记作pro4
await Promise.resolve().then(() => console.log('t2p'));
// 记作await pro4
console.log(4);
}
t1();
// 记作pro1
new Promise(function(resolve) {
console.log('promise1')
resolve();
}).then(function() {
console.log('promise2')
})
t2();
console.log('end');
// 打印结果
1
promise3
b
promise1
3
end
promise4
t1p
promise2
t2p
2
primise5
4
promise6
setTimeout
代码执行流程:
set回调
进到宏任务队列;执行
t1()
,打印1
,打印promise3
,将pro2的then回调
放到微任务队列;打印
b
,将pro3的then回调
放到微任务队列,因为await
,之后的代码在pro3的then回调
执行后再执行;打印
promise1
,将pro1的then回调
放到微任务队列;执行
t2()
,打印3
,将pro4的then回调
放到微任务队列,pro4
之后的代码暂不执行;打印
end
。此时的微任务队列为:微任务队列 pro2的then回调 pro3的then回调 pro1的then回调 pro4的then回调 接着开始依次执行微任务队列中的回调。执行
pro2的then回调
,打印promise4
;执行
pro3的then回调
,打印t1p
,将await pro3
代码块放到微任务队列;执行
pro1的then回调
,打印promise2
;执行
pro4的then回调
,打印t2p
,将await pro4
代码放到微任务队列;执行
await pro3
代码块,打印2
,打印promise5
,将pro5的then回调
放到微任务队列;执行
await pro4
,打印4
;执行
pro5的then回调
,打印promise6
;微任务队列代码执行完毕,读取宏任务队列,执行
set回调
,打印setTimeout
。
知识点总结
async/await本质上还是基于Promise的一些封装, 可以理解为,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的回调。
await之后的代码必须等await语句执行完成后(包括微任务完成),才能执行后面的,也就是说,只有运行完await语句,才把await语句后面的全部代码加入到微任务队列。