一、js的执行机制
众所周知:JavaScript 是一门单线程语言,因为是单线程,所以代码应该是自上而下执行的,事实是不是如此,先看下列一段代码:
setTimeout(() => {
console.log('set1')
});
new Promise((resolve, reject) => {
console.log('p1');
resolve();
}).then(() => {
console.log('then1')
});
console.log(1);
// 输出: p1 1 then1 set1
上面的代码输出顺序就被打乱了,为什么输出的顺序并不是按照代码的顺序,那么下面的部分会为你解惑。
1.任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。
2.事件循环
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop
(事件循环)。
3.宏任务和微任务
两个任务分别处于任务队列中的宏队列与微队列中;
宏队列
与微队列
组成了任务队列;
任务队列
将任务放入执行栈
中执行;
宏任务
- 包括整体代码 script
- setTimeout
- setInterval
- setImmediate(Node独有)
- I/O
- UI 交互事件(浏览器独有)
- requestAnimationFrame (浏览器独有)
微任务
- Promise
- process.nextTick (Node独有)
- MutationObserver (监听DOM树变化)
- Object.observe (异步监视对象修改,已废弃)
例子:
console.log('1: script start');
setTimeout(() => {
console.log('2: setTimeout1');
new Promise((resolve) => {
console.log('3: promise1');
resolve();
}).then(() => {
console.log('4: then1')
})
});
new Promise((resolve) => {
console.log('5: promise2')
resolve();
}).then(() => {
console.log('6: then2');
setTimeout(() => {
console.log('7: setTimeout2')
})
})
console.log('8: script end')
第一轮事件循环流程如下:
- 整体script作为第一个宏任务进入主线程,遇到console,输出
1: script start
。
- 遇到
setTimeout
, 其回调函数分发到宏任务事件队列中,我们暂时标记为setTimeout1
。
- 遇到
promise
,new Promise
直接执行,输出5: promise2
,then
分发到微任务队列中,
- 遇到
console
,输出8: script end
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | then2 |
- 上表是第一轮事件循环宏任务结束的时候各 Event Queue 的情况,此时已经输出了 1, 5,8。
- 我们发现then2 微任务,执行输出
6: then2
, 遇到setTimeout
,将其分发至宏任务队列中,标记为setTimeout2
, 第一轮事件循环结束。
- 第一轮循环正式结束,输出了 1,5,8,6。
第二轮事件从 setTimeout1
宏任务开始:
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout1 | |
setTimeout2 |
- 首先输出
2: setTimeout1
, 然后遇到Promise
,立即执行new Promise
,输出3: promise1
, 将then
分发到微任务队列中,标记为then1
。setTimeout1执行结束,此时队列中事件如下:
宏任务Event Queue | 微任务Event Queue |
---|---|
setTimeout2 | then1 |
- 第二轮宏任务执行结束,我们发现有
then1
微任务可以执行。
- 输出
4: then1
。
- 第二轮循环结束,第二轮输出2,3,4。
第三轮事件循环开始,此时只剩 setTimeout2
了,执行。
- 直接输出 7: setTimeout2
整段代码,共进行了三次事件循环,完整输出1,5,8,6,2,3,4,7
async/await (重点)
console.log(1);
async function fn(){
console.log(2);
await console.log(3);
console.log(4);
}
setTimeout(()=>{
console.log(5);
},0)
fn();
new Promise((resolve)=>{
console.log(6);
resolve();
}).then(()=>{
console.log(7);
})
console.log(8);
async
当我们在函数前使用async
的时候,使得该函数返回的是一个Promise
对象:
async function test() {
return 1 // async的函数会在这里帮我们隐式使用Promise.resolve(1)
}
// 等价于下面的代码
function test() {
return new Promise(function(resolve, reject) {
resolve(1)
})
}
可见async
只是一个语法糖,只是帮助我们返回一个Promise
而已.
Promise和async中的立即执行
我们知道Promise中的异步体现在then和catch中,所以写在Promise中的代码是被当做同步任务立即执行的。而在async/await中,在出现await出现之前,其中的代码也是立即执行的。那么出现了await时候发生了什么呢?
await
await
表示等待,是右侧「表达式」的结果,这个表达式的计算结果可以是 Promise 对象也可以是其他值(换句话说,就是没有特殊限定)。并且只能在带有 async
的内部使用.
很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的表达式会先执行一遍,将await后面的代码加入到microtask中,然后就会跳出整个async函数来执行后面的代码。
通俗的解释一下:
使用await时,会从右往左执行,当遇到await时, <font color="red">
★★★★★会阻塞函数内部处于它后面的代码,去执行该函数外部的同步代码,当外部同步代码执行完毕,再回到该函数内部执行剩余的代码★★★★★
</font>, 并且当await执行完毕之后,会先处理微任务队列的代码
修改一下之前的面试题:
console.log(1);
async function fn(){
console.log(2)
new Promise((resolve)=>{
resolve();
}).then(()=>{
console.log("XXX")
})
await console.log(3)
console.log(4)
}
fn();
new Promise((resolve)=>{
console.log(6)
resolve();
}).then(()=>{
console.log(7)
})
console.log(8)
// 执行结果为:1 2 3 6 8 XXX 4 7
修改2
console.log(1);
new Promise((resolve)=>{
resolve();
}).then(()=>{
console.log("XXX")
})
async function fn(){
console.log(2)
await console.log(3)
console.log(4)
new Promise((resolve)=>{
resolve();
}).then(()=>{
console.log("YYY")
})
}
fn();
new Promise((resolve)=>{
console.log(6)
resolve();
}).then(()=>{
console.log(7)
})
console.log(8)
// 执行结果为:1 2 3 6 8 XXX 4 7 YYY
由此可见,代码执行的时候,只要碰见 await ,都会执行完当前的 await 之后,把 await 后面的代码放到微任务队列里面。
回到文章最初的面试题,是不是在 console.log(4) 前面加上 await,4 是不是就可以在 3 之后打印出来了?
console.log(1);
async function fn(){
console.log(2)
await console.log(3)
await console.log(4)
}
setTimeout(()=>{
console.log(5)
},0)
fn();
new Promise((resolve)=>{
console.log(6)
resolve();
}).then(()=>{
console.log(7)
})
console.log(8)
// 执行结果为:1 2 3 6 8 4 7 5
可见,代码执行的时候,只要是碰见 await,执行完当前的 await 的代码(即 await console.log(3))之后,在 await 之后的代码(即 await console.log(4))都会被放到微任务队列里面。
如果在 await console.log(4) 后面再加上 await 的其他代码呢?
console.log(1);
async function fn(){
console.log(2)
await console.log(3)
await console.log(4)
await console.log("await之后的:",11)
await console.log("await之后的:",22)
await console.log("await之后的:",33)
await console.log("await之后的:",44)
}
setTimeout(()=>{
console.log(5)
},0)
fn();
new Promise((resolve)=>{
console.log(6)
resolve();
}).then(()=>{
console.log(7)
})
console.log(8)
/**
* 执行结果为:
* 1
* 2
* 3
* 6
* 8
* 4
* 7
* await之后的: 11
* await之后的: 22
* await之后的: 33
* await之后的: 44
* 5
*/
可见当不断碰见 await ,把 await 之后的代码不断的放到微任务队列里面的时候,代码执行顺序是会把微任务队列执行完毕,才会去执行宏任务队列里面的代码。