首先给出如下代码:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
new Promise(resolve => {
console.log(1);
resolve();
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(4);
});
其输出顺序为:async1 start, async2, 1, 2, 3, async1 end, 4
问题主要涉及以下 4 点:
- Promise 的链式 then() 是怎样执行的
- async 函数的返回值
- await 做了什么
- PromiseResolveThenableJob:浏览器对 new Promise(resolve => resolve(thenable)) 的处理
Promise 的链式 then 调用是怎样执行的
new Promise(r => {
r();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3));
new Promise(r => {
r();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6));
/*
输出:
1
4
2
5
3
6
*/
- Promise 多个 then() 链式调用,并不是连续的创建了多个微任务并推入微任务队列,因为 then() 的返回值必然是一个 Promise,而后续的 then() 是上一步 then() 返回的 Promise 的回调
- 传入 Promise 构造器的执行器函数内部的同步代码执行到 resolve(),将 Promise 的状态改变为
<resolved>: undefined
, 然后 then 中传入的回调函数 console.log(‘1’) 作为一个微任务被推入微任务队列 - 第二个 then() 中传入的回调函数 console.log(‘2’) 此时还没有被推入微任务队列,只有上一个 then() 中的 console.log(‘1’) 执行完毕后,console.log(‘2’) 才会被推入微任务队列
总结一下就是:
- Promise.prototype.then() 会隐式返回一个新 Promise
- 如果 Promise 的状态是 pending,那么 then 会在该 Promise 上注册一个回调,当其状态发生变化时(resolve 或 reject 函数执行时),对应的回调将作为一个微任务被推入微任务队列
- 如果 Promise 的状态已经是 fulfilled 或 rejected,那么 then() 会立即创建一个微任务,将传入的对应的回调推入微任务队列
上面的例子中,第一个 then 回调函数是在 pending 状态注册的,但是是在 r()函数执行之后才被推入到微任务队列里的,而第二个 then 函数是在第一个 then 函数执行之后,此时返回的新的 Promise 以及变为 fulfilled 状态了,就会立即创建一个微任务
async/await 可视为 Promise 的语法糖,同样基于微任务实现;本题主要纠结的点在于 await 到底做了什么导致 async1 end 晚于 promise2 输出。问题的关键在于其执行过程中的微任务数量
解释 async 关键字做了什么:
- 被 async 操作符修饰的函数必然返回一个 Promise
- 当 async 函数返回一个值时,Promise 的 resolve 方法负责传递这个值
- 当 async 函数抛出异常时,Promise 的 reject 方法会传递这个异常值
async2 等价转换:
function async2() {
console.log("async2");
return Promise.resolve();
}
对于 await v:
- await 后的值 v 会被转换为 Promise,并在这个新 Promise 中 resolve(v)
- await v 后续的代码的执行类似于传入 then() 中的回调
将 async1 等价转换:
function async1() {
console.log("async1 start");
return new Promise(resolve => resolve(async2())).then(() => {
console.log("async1 end");
});
}
return new Promise(resolve => resolve(async2())) 中,Promise resolve 的是 async2(),而 async2() 返回了一个状态为 <resolved>: undefined
的 Promise,Promise 是一个 thenable 对象。
对于 PromiseResolveThenableJob 对象,MDN 上是这样解释的:
- 对于一个对象 o,如果 o.then 是一个 function,那么 o 就可以被称为 thenable 对象
- 对于 new Promise(resolve => resolve(thenable)),即“在 Promise 中 resolve 一个 thenable 对象”,需要先将 thenable 转化为 Promise,然后立即调用 thenable 的 then 方法,并且 这个过程需要作为一个 job 加入微任务队列,以保证对 then 方法的解析发生在其他上下文代码的解析之后
let thenable = {
then(resolve, reject) {
//注册微任务in thenable
console.log("in thenable");
resolve(100); //执行的时候注册微任务thenable ok并将其推入微任务队列
},
};
new Promise(resolve => {
console.log("in p0"); // 宏任务1
resolve(thenable);
}).then(() => {
console.log("thenable ok");
});
new Promise(resolve => {
console.log("in p1"); // 宏任务2
resolve(); //注册微任务1
})
.then(() => {
console.log("1");
})
.then(() => {
console.log("2");
})
.then(() => {
console.log("3");
})
.then(() => {
console.log("4");
});
/*
in p0
in p1
in thenable
1
thenable ok
2
3
4
*/
- in thenable 后于 in p1 而先于 1 输出,同时 thenable ok 在 1 后输出
- 在执行完同步任务后,微任务队列中只有 2 个微任务:第一个是 转换 thenable 为 Promise 的过程,即 PromiseResolveThenableJob,也即是“in thenable”,第二个是 console.log(‘1’)
- 在 PromiseResolveThenableJob 执行中会执行 thenable.then(),从而注册了另一个微任务:console.log(‘thenable ok’)
- 正是由于规范中对 thenable 的处理需要在一个微任务中完成,从而导致了第一个 Promise 的后续回调(即第一个 Promise.then 回调函数)被延后了 1 个时序
总结一下就是:
thenable 对象里的 then 函数会被当成微任务,导致原来的 Promise 后的 then 回调被延迟推入到微任务队列中(示例中是在 thenable 函数执行 resolve 后,”thenable ok”才被放入微任务里),所以当第一次微任务队列被清空后(输出 1),此时才输出“thenabel ok”
如果在 Promise 中 resolve 一个 Promise 实例呢?
由于 Promise 实例是一个对象,其原型上有 then 方法,所以这也是一个 thenable 对象。同样的,浏览器会创建一个 PromiseResolveThenableJob 去处理这个 Promise 实例,这是一个微任务。在 PromiseResolveThenableJob 执行中,执行了 Promise.prototype.then
,而这时 Promise 如果已经是 resolved 状态 ,then 的执行会再一次创建了一个微任务。
最终结果就是:额外创建了两个 Job,表现上就是后续代码被推迟了 2 个时序
为什么这里被延迟了 2 个时序而不是 1 个呢?原因是:1. 这个 resolve 的 Promise 实例会创建一个 thenable 对象,这是一个微任务。2. 在这个 thenable 执行过程中,这个 Promise 实例会调用 then 方法并且已经是 resolved 状态(以保证后续的外层的 Promise 的 then 方法能够正常执行下去),这又会创建一个微任务。综上所以是延迟了 2 个时序
async 函数处理返回值的问题,它会像 Promise.prototype.then 一样,会对返回值的类型进行辨识。
结论
async 函数在抛出返回值时,会根据返回值类型开启不同数目的微任务
- return 结果值:非 thenable、非 promise(不等待)
- return 结果值:thenable(等待 1 个 then 的时间)
- return 结果值:promise(等待 2 个 then 的时间)
await 右值类型区别:
- await 后面接非 thenable 类型,会立即向微任务队列添加一个微任务 then,但不需等待
- await 后面接 thenable 类型,需要等待一个 then 的时间之后执行
- await 后面接 promise 类型,其表现和非 thenable 一样
new Promise(resolve => {
resolve(thenable);
});
//转换:
new Promise(resolve => {
Promise.resolve().then(() => {
thenable.then(resolve);
});
});
//new Promise(resolve => resolve(async2())):
new Promise(resolve => {
Promise.resolve().then(() => {
async2().then(resolve);
});
});
//最终转换:
function async1() {
console.log("async1 start");
const p = async2(); //返回一个Promise.resolve()
return new Promise(resolve => {
Promise.resolve().then(() => {
//注册了一个微任务
p.then(resolve); //又注册了一个微任务
});
}).then(() => {
console.log("async1 end"); //再注册了一个微任务(前面注册的两个微任务导致这个微任务延后2个时序输出)
});
}
function async2() {
console.log("async2");
return Promise.resolve();
}
async1();
new Promise(resolve => {
//宏任务
console.log(1);
resolve();
})
.then(() => {
//注册微任务consoel.log(2)
console.log(2);
})
.then(() => {
//注册微任务consoel.log(3)
console.log(3);
})
.then(() => {
////注册微任务consoel.log(4)
console.log(4);
});
/*
async1 start
async2
1
2
3
async1 end
4
*/
补充
-
Promise.resolve(v) 不等于 new Promise(r => r(v)),因为如果 v 是一个 Promise 对象,前者会直接返回 v,而后者需要经过一系列的处理(主要是 PromiseResolveThenableJob)
-
宏任务的优先级是高于微任务的,而原题中的 setTimeout 所创建的宏任务可视为第二个宏任务,第一个宏任务是这段程序本身