Abo

宏观任务[MacroTask]和微观任务[MicroTask]的执行

换个角度:setTimeout跟promise跟promise.then以及async跟await中代码的执行先后顺序为?

说白了:就是promise还有generator跟async三者的用法

wallhaven-ox2pm5

问题等价于理解消息队列与事件循环

前言

1

整个串起来

Philip搞了一个的碉堡的工具来可视化这个过程,这玩意儿叫Loupe。这是一个能够把JavaScript运行时可视化的工具。

我们用它来看一个简单的例子:在一个异步的setTimeout回调中用console.log在控制台打些log出来。

clipboard.png

整个过程到底都发生了什么呢?我们来看一下:

  1. 执行进入console.log('Hi');函数,因此这个函数被丢进了调用栈里。
  2. console.log('Hi');函数return了,因此他就被弹出了栈顶。
  3. 执行进入setTimeout函数,因此这个函数被丢进了调用栈里。
  4. setTimeoutWeb APIs的一部分,因此Web APIs处理了他,并且等了2秒
  5. 继续执行脚本,进入console.log('EvenyBody')函数,把他也丢进调用栈。
  6. console.log('EvenyBody')函数return了,所以把他从栈顶弹出去
  7. 2秒的定时已经完成了,所以就把对应的回调函数放到回调队列里。
  8. 事件循环检查调用栈是否为空,如果非空的话,他就等着。因为调用栈现在是空的,所以把回调队列中的回调函数丢进调用栈。
  9. console.log('There')函数返回了,因此把他从栈顶弹出去(译者按:原文为console.log(‘Everybody’),应为书写错误)。

有趣的一点是:setTimeout(function(...), 0)的情况。setTimeout为0的时候这个过程看起来可能不明显,除非考虑到调用栈的执行环境和事件循环的情况。基本上都会推迟到调用栈为空才执行。

宏任务和微任务执行完成后都会判断是否还有微任务,有的话执行微任务,没有就执行宏任务,如此循坏

面试题实战

promise

1
2
3
4
5
6
7
new Promise(resolve => {
console.log(1);
resolve(3);
}).then(num => {
console.log(num)
});
console.log(2)

这道题的输出是123,为什么不是132呢?因为我一直理解Promise是没有异步功能,它只是帮忙解决异步回调的问题,实质上是和回调是一样的,所以如果按照这个想法,resolve之后应该会立刻then。但实际上并不是。难道用了setTimeout?

如果在promise里面再加一个promise:

1
2
3
4
5
6
7
8
new Promise(resolve => {
console.log(1);
resolve(3);
Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2)

执行顺序是1243,第二个Promise的顺序会比第一个的早,所以直观来看也是比较奇怪,这是为什么呢?

  • 不哔哔,搞段代码瞅瞅:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(function() {
console.log('setTimeout');
})

new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})

console.log('console');

// promise
// console
// then
// setTimeout
  • 首先会遇到setTimeout,将其放到宏任务event queue里面
  • 然后回到 promise , new promise 会立即执行, then会分发到微任务
  • 遇到 console 立即执行
  • 整体宏任务执行完成,接下来判断是否有微任务
    ,刚刚放到微任务里面的then,执行
  • ok,第一轮事件结束,进行第二轮,刚刚我们放在event queue 的setTimeout 函数进入到宏任务,立即执行
  • 结束

再来两道题看看:

1
2
3
4
5
6
7
8
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
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

动画演示参考

再来一道涉及冒泡的题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});

// Here's a click listener…
function onClick() {
console.log('click');

setTimeout(function() {
console.log('timeout');
}, 0);

Promise.resolve().then(function() {
console.log('promise');
});

outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

click
promise
mutate
click
promise
mutate
timeout
timeout

终于结束了,我们来贴段巨复杂的代码搞一搞

process.nextTick(callback)类似node.js版的”setTimeout”,在事件循环的下一次循环中调用 callback 回调函数。

除了广义的同步任务和异步任务,我们可以分的更加精细一点:

  • microtasks: setTimeout, setInterval, setImmediate, I/O, UI rendering
    microtasks: process.nextTick, Promise, MutationObserver 不同的任务会进入到不同的event queue。比如setTimeout和setInterval会进入相同的Event Queue。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})

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,3,5,9,11,10,12

惊不惊喜,意不意外,我们来分析一下

  • 首先先执行console.log(1) 然后将setTimeout放到宏任务event queue里面 记作 setTimeout 1 ,接着 看到 process.nextTick ,将其放到微任务里面 ,记作 process 1,然后 看到new promise 立即执行输出9 ,将里面的then 放到 微任务里面 记作 then 2, 继续,遇到 setTimeout 放到宏任务里面记作 setTimeout 2 。目前输出的是:1,7,
  • OK, 接下来,开始判断是否有微任务,刚刚放入到微任务event queue的进入到主程序开始执行,process 1 , then 2 目前输出的是:6,8
  • 接下来,微任务的event queue 空了,进行下一轮事件,将刚刚放到宏任务的 setTimeout 1 进入到主线程
    遇到 console 立即执行, 遇到 process.nextTick 放到微任务 event queue 里面 记作 process1, 接着遇到 new Promise 立即执行, 将 then 放到event queue 里面 记作 then 2,OK,当前宏任务里的任务执行完了,判断是否有微任务,发现有 process1, then 2 两个微任务 , 一次执行 目前输出的是:2,4,3,5
  • 目前主线程里的任务都执行结束了,又开始第三轮事件循环,同上(字太多,省略。。。。) 目前输出的是:9,11,10,12

注意: 以上所说只能是在浏览器中的执行顺序,

async

1
2
3
4
async function asyncFn() {
  return 'hello world';
}
asyncFn();

这样就表示这是异步函数,返回的结果

2

返回的是一个promise对象,状态为resolved,参数是return的值。那再看下面这个函数

1
2
3
4
5
6
7
async function asyncFn() {
return '我后执行'
}
asyncFn().then(result => {
console.log(result);
})
console.log('我先执行');

3

async 表示函数里有异步操作

await 表示紧跟在后面的表达式需要等待结果

还有一点,在async里,必须要将结果return回来,不然的话不管是执行reject还是resolved的值都为undefine,建议使用箭头函数。

其余返回结果都是判定resolved成功执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//正确reject方法。必须将reject状态return出去。
async function PromiseError() {
return Promise.reject('has Promise Error');
}

//这是错误的做法,并且判定resolve,返回值为undefined,并且Uncaught报错
async function PromiseError() {
Promise.reject('这是错误的做法');
}


async function d(){
'这个值接收不到'
}
d().then(success => console.log('成功',success));
//成功 undefined
//Promise { <resolved>: undefined }
-----------------------------------------------------------
async function e(){
return '接收到了'
}
e().then(success => console.log('成功',success));
//成功 接收到了
//Promise { <resolved>: undefined }

最后一行Promise { <resolved> : undefined } 是因为返回的是console.log执行语句,没有返回值。

控制台直接运行代码,会返回最后一句代码的返回值

await

await意思是async wait(异步等待)。这个关键字只能在使用async定义的函数里面使用。任何async函数都会默认返回promise,并且这个promise解析的值都将会是这个函数的返回值,而async函数必须等到内部所有的 await 命令的 Promise 对象执行完,才会发生状态改变。

打个比方,await是学生,async是校车,必须等人齐了再开车。

就是说,必须等所有await 函数执行完毕后,才会告诉promise我成功了还是失败了,执行then或者catch

很多人以为await会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await后面的函数会先执行一遍(比如await Fn()的Fn ,并非是下一行代码),然后就会跳出整个async函数来执行后面js栈的代码。等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入Promise队列(Promise的Job Queue)

来看个简单点的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
const timeoutFn = function(timeout){ 
return new Promise(function(resolve){
return setTimeout(resolve, timeout);
});
}

async function fn(){
await timeoutFn(1000);
await timeoutFn(2000);
return '完成';
}

fn().then(success => console.log(success));

这里本可以用箭头函数写方便点,但是为了便于阅读本质,还是换成了ES5写法,上面执行函数内所有的await函数才会返回状态,结果是执行完毕3秒后才会弹出’完成‘。

正常情况下,await 命令后面跟着的是 Promise ,如果不是的话,也会被转换成一个 立即 resolve 的 Promise。

也可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function timeout(time){
return new Promise(function(resolve){
return setTimeout(function(){
return resolve(time + 200)
},time);
})
}

function first(time){
console.log('第一次延迟了' + time );
return timeout(time);
}
function second(time){
console.log('第二次延迟了' + time );
return timeout(time);
}
function third(time){
console.log('第三次延迟了' + time );
return timeout(time);
}

function start(){
console.log('START');
const time1 = 500;
first(time1).then(time2 => second(time2) )
.then(time3 => third(time3) )
.then(res => {
console.log('最后一次延迟' + res );
console.timeEnd('END');
})
};
start();

这样用then链式回调的方式执行resolve

1
2
3
4
5
6
7
8
//打印结果

START
第一次延迟了500
第二次延迟了700
第三次延迟了900
最后一次延迟1100
END

用async/await呢?

1
2
3
4
5
6
7
8
9
10
async function start() {
console.log('START');
const time1 = 500;
const time2 = await first(time1);
const time3 = await second(time2);
const res = await third(time3);
console.log(`最后一次延迟${res}`);
console.log('END');
}
start();

达到了相同的效果。但是这样遇到一个问题,如果await执行遇到报错呢

async里如果有多个await函数的时候,如果其中任一一个抛出异常或者报错了,都会导致函数停止执行,直接reject;

怎么处理呢,可以用try/catch,遇到函数的时候,可以将错误抛出,并且继续往下执行。

1
2
3
4
5
6
7
8
9
10
11
let last;
async function throwError() {
try{
await Promise.reject('error');
last = await '没有执行';
}catch(error){
console.log('has Error stop');
}
}
throwError().then(success => console.log('成功', last))
.catch(error => console.log('失败',last))

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function testSometing() {
console.log("testSomething");
return "return testSomething";
}

async function testAsync() {
console.log("testAsync");
return Promise.resolve("hello async");
}

async function test() {
console.log("test start...");

const testFn1 = await testSometing();
console.log(testFn1);

const testFn2 = await testAsync();
console.log(testFn2);

console.log('test end...');
}

test();

var promiseFn = new Promise((resolve)=> {
console.log("promise START...");
resolve("promise RESOLVE");
});
promiseFn.then((val)=> console.log(val));

console.log("===END===")

执行结果

4

我们一步步来解析

首先test()打印出test start...

然后 testFn1 = await testSomething(); 的时候,会先执行testSometing()这个函数打印出“testSometing”的字符串。

之后因为await会让出线程就会去执行后面的。testAsync()执行完毕返回resolve,触发promiseFn打印出“promise START...”。

接下来会把返回的Promiseresolve("promise RESOLVE")放入Promise队列(Promise的Job Queue),继续执行打印“===END===”。

等本轮事件循环执行结束后,又会跳回到async函数中(test()函数),等待之前await 后面表达式的返回值,因为testSometing() 不是async函数,所以返回的是一个字符串“return`testSometing`”。

test()函数继续执行,执行到testFn2(),再次跳出test()函数,打印出“testAsync”,此时事件循环就到了Promise的队列,执行promiseFn.then((val)=> console.log(val));打印出“promise RESOLVE”。

之后和前面一样 又跳回到test函数继续执行console.log(testFn2)的返回值,打印出“hello async”。

最后打印“test end...”。

加点料,让testSomething()变成async

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async function testSometing() {
console.log("testSomething");
return "return testSomething";
}

async function testAsync() {
console.log("testAsync");
return Promise.resolve("hello async");
}

async function test() {
console.log("test start...");

const testFn1 = await testSometing();
console.log(testFn1);

const testFn2 = await testAsync();
console.log(testFn2);

console.log('test end...');
}

test();

var promiseFn = new Promise((resolve)=> {
console.log("promise START...");
resolve("promise RESOLVE");
});
promiseFn.then((val)=> console.log(val));

console.log("===END===")

执行结果:

5

和上一个例子比较发现promiseFn.then((val)=> console.log(val));先于console.log(testFn1)执行。

原因是因为现在的版本async函数会被await resolve,

testSomething()已经是async函数,返回的是一个Promise对象需要等它 resolve 后将当前Promise 推入队列,随后先清空调用栈,所以会”跳出” test() 函数执行后续代码,随后才开始执行该 Promise。

【题目】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start');
setTimeout(function(){
console.log('setTimeout')
},0);
async1();
new Promise(function(resolve){
console.log('promise1');
resolve();
}).then(function(){
console.log('promise2')
});
console.log('script end')

答案:

1
2
3
4
5
6
7
8
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

事实上,没有在控制台执行打印之前,我觉得它应该是这样输出的:

1
2
3
4
5
6
7
8
script start
async1 start
async2
async1 end
promise1
script end
promise2
setTimeout

为什么这样认为呢?因为我们(粗浅地)知道await之后的语句会等await表达式中的函数执行完得到结果后,才会继续执行。

MDN是这样描述await的:

async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。

会认为输出结果是以上的样子,是因为没有真正理解这句话的含义。

阮一峰老师的解释我觉得更容易理解:

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

对啦就是这样,MDN描述的暂停执行,实际上是让出了线程(跳出async函数体)然后继续执行后面的脚本的。这样一来我们就明白了,所以我们再看看上面那道题,按照这样描述那么他的输出结果就应该是:

1
2
3
4
5
6
7
8
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

关于await前后可以理解成一个 new promise的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原来 async 内部的代码
// await 上面的代码
doA()
await xxx
// await 下面的代码
doB()

// 相当于
new Promise(resolve => {
// 创建 promise 实例的时候,放的是 await 前面的代码
doA()
// await 后面的表达式相当于 resolve(xxx)
resolve(xxx)
}).then(data => {
// await 下面的代码
doB()
})

同步的请求、页面上会变成静止状态、也就是拿不到响应之前、不允许用户操作其他东西

async+await其实还是异步、

这就变成同步和异步的区别了。。。

举个例子、

相当于你是老板、同步就是你自己去买菜了、买了菜回来、才可以开会之类的做公司的事情

异步就相当于、你一边在网上买菜一边工作、网上的菜什么时候送来是根据当时的路况和其他因素、你该开会开会、该工作工作、不会被阻塞