再探Promise之Promise与微任务
时常会遇到一些判断代码输出顺序的异步问题,主要会有promise+定时器、纯promise、await+promise、nextTick+Promise(node)等类型,这篇文章会逐个进行研究
目前Q&A(看看有你需要了解的部分吗)
- Q1:then的回调函数什么时候进入的微任务队列?
- Q2:Await/Async是怎样对待微任务的?
- Q3:then回调函数如果返回一个Promise/thenable,会造成什么影响吗?
- Q4:
Promise.resolve()
与new Promise((_res,_rej)=>(_res()))
有什么不同? - Q5:在Promie中'递归'resolve(Promise)会怎样?
纯promise
示例一(链式调用创Promise)
ts
new Promise((resolve, reject) => {
console.log(1);
resolve(1);
})
.then(a => {
console.log(2);
new Promise((resolve, reject) => {
//内层Promise
console.log(3);
resolve(1);
})
.then(c => {
console.log(4);
})
.then(d => {
console.log(6);
});
})
.then(b => {
console.log(5);
});
export {};
这部分很容易能得出下面打印过程
- 首先script宏任务开始,打印
1
,并将Promise状态变为终态- 继续执行,遇到第一个then(a),由于调用这个then的promise已经为终态,所以a直接加入微任务队列,继续执行,调用b时,由于b对应的promise还是pending,所以放入调用then(b)的promise的缓存队列中
- 第一轮宏任务执行完毕,开始扫描微任务队列,此时微任务队列为[a]
- 从微任务队列中取出a,执行a,打印
2
,打印3
,内层Promise由于也resolve()变为终态- 继续执行,遇到内层Promise的then(c),由于调用c的promise已变为终态,所以c直接进入微任务队列,此时微任务队列:[c]
- 继续执行,遇到d,此时调用d的promise还处于pending,所以放入调用d所对应的promise的缓存队列
- a执行完毕,b所对应的promise进入终态,b进入微任务队列,此时微任务队列为[c,b]
- 取出c,执行c,打印
4
,c执行完毕,d所对应的promise变为终态d,d进入微任务队列,此时微任务队列为[b,d]- 取出b,执行b,打印
5
,b执行完毕- 取出d,执行d,打印
6
,d执行完毕- 本次tick结束
promise+定时器
示例一(宏任务创建微任务)
ts
console.log('script-宏任务-start');
const p11 = new Promise((_res, _rej) => {
console.log('promise-p11-setTimeout-before');
setTimeout(() => {
console.log('promise-p11-setTimeout-start');
_res(1);
console.log('promise-p11-setTimeout-end');
}, 1000);
console.log('promise-p11-setTimeout-after');
});
p11.then(function mirrTaskFn(v) {
console.log('p11-then');
});
console.log('script-宏任务-end');
export {};
从宏任务开始,遇到微任务先放进微任务队列(根据微任务优先级可以进行"插队"),等本轮宏任务执行完毕,挨个执行微任务,中间如果又产生微任务依旧在本轮执行完,什么时候微任务队列为空的时候,再执行下一轮宏任务
所以执行流程为
- 第一轮
- 宏任务:
script-宏任务-start
->promise-p11-setTimeout-before
->promise-p11-setTimeout-after
->script-宏任务-end
- 微任务:未创建微任务
- 第二轮
- 宏任务:
promise-p11-setTimeout-start
->promise-p11-setTimeout-end
- 微任务:创建微任务一个,微任务队列[mirrTaskFn],等待宏任务执行完毕,清空微任务队列,第二轮打印总的打印顺序为
promise-p11-setTimeout-start
->promise-p11-setTimeout-end
->p11-then
示例二(微任务创建微任务)
ts
console.log('script-宏任务-start');
const p_1 = new Promise((_res, _rej) => {
console.log('p_1 setTimeout before');
setTimeout(() => {
console.log('p_1 setTimeout start');
_res(1);
});
});
const p_1_2 = p_1.then(function mirrTaskFn_1(v) {
console.log('p_1-then');
});
const p_1_3 = p_1_2.then(function mirrTaskFn_1_2(v) {
console.log('p_1_2-then');
});
const p_2 = new Promise((_res, _rej) => {
_res(1);
});
const p_2_1 = p_2.then(function mirrTaskFn_2(v) {
console.log('p_2-then');
});
const p_2_2 = p_2_1.then(function mirrTaskFn_2_1(v) {
console.log('p_2_1-then');
});
console.log('script-宏任务-end');
export {};
执行流程为:
- 第一轮
- 宏任务:
script-宏任务-start
->p_1 setTimeout before
->script-宏任务-end
- 微任务:
- 由于在调用
p_2.then
时,p_2已经变为了终态,所以直接加入微队列,即[mirrTaskFn_2]- 在本轮宏任务执行完毕之后将会执行微任务队列任务,在执行mirrTaskFn_2之后,p_2_1变为终态,所以会直接将mirrTaskFn_2_1加入微队列,即在mirrTaskFn_2执行完毕之后,微任务队列变为[mirrTaskFn_2_1]
- 等到mirrTaskFn_2_1执行完毕,没有新的微任务创建,第一轮执行完成。
- 第一轮总的打印顺序为
script-宏任务-start
->p_1 setTimeout before
->script-宏任务-end
->p_2-then
->p_2_1-then
- 第二轮
- 宏任务:
p_1 setTimeout start
- 微任务:由于在第一轮已经调用了p_1.then和p_1_2.then,此时
mirrTaskFn_1
和mirrTaskFn_1_2
已经在各自的缓存队列([[PromiseFulfillReactions]])中了,现在p_1
已经变为了终态,那么mirrTaskFn_1
将会被加入微队列,此时微队列为[mirrTaskFn_1]。- 等到mirrTaskFn_1执行完毕,返回的promise就变为终态了,此时mirrTaskFn_1_2进入微队列,即[mirrTaskFn_1_2]。
- 等到mirrTaskFn_2_1执行完毕,没有新的微任务创建,第二轮执行完成。
- 第二轮总的打印顺序为
p_1 setTimeout start
->p_1-then
->p_1_2-then
Q1:then的回调函数什么时候进入的微任务队列?
需要分为两种情况
-
如果调用then的时候promise是pending状态,那么then的回调不会被添加微任务,而是进入[[PromiseFulfill/RejectReactions]] 列表里缓存起来,缓存起来的是条链表(为了处理多个then的链式调用)等到promise的状态发生更改(reslove/reject)时,会将该列表中的链表给取出来挨个放进微任务中(放到下轮事件循环处理)
tsconst p11 = new Promise((_res, _rej) => { setTimeout(() => { _res(1); }, 1000); }); p11.then(function mirrTaskFn(v){ console.log('p11-then'); });
-
如果调用then的时候promise已经是最终态,那么直接放入微任务队列中(本轮事件循环会处理)
tsconst p11 = new Promise((_res, _rej) => { _res(1); }); p11.then(function mirrTaskFn(v){ console.log('p11-then'); });
Await + Promise
示例一(入门级)
ts
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(2);
}).then(function () {
console.log('promise2');
});
console.log('script end');
export {};
分析可以得出
第一轮
- 宏任务:
script start
->async1 start
->async2
->promise1
->script end
,到此本次tick中宏任务结束- 微任务:
async1 end
->promise2
第二轮
宏任务:
setTimeout
微任务:无
Q2:Await/Async是怎样对待微任务的?
首先让我们从几个简单的例子去理解
-
async 返回非Promise
tsasync function asyncFn() { return '1'; }
我们知道async返回的是一个Promise,如果async关键字函数返回的不是promise,会自动用Promise.resolve()包装,与之对应的Promise写法如下
tsfunction asyncFn() { return Promise.resolve('1'); }
-
async 返回Promise
tsasync function asyncFn() { return new Promise((_res, _rej) => { _res(1); }); }
如果async返回的是一个Promise,不会采用Promise.resolve()去包装,而是采用
new Promise((_res,_rej)=>(_res()))
来包装,这里涉及到两者的区别(也就是Q4)tsfunction asyncFn() { return new Promise((_res2, _rej2) => { _res2( new Promise((_res, _rej) => { _res(1); }) ); }); }
-
await 一个非Promise
tsasync function async1(){ console.log(1); await 1; console.log(2); }
await后面如果是非Promise,那么就直接返回对应的值;不管await后面跟着的是什么,awiat都会阻塞后面的代码
tsfunction async1() { console.log(1); return Promise.resolve(1).then(() => { //await后面的代码会被放入微任务队列 console.log(2); }); }
-
await Promise
tsasync function async1(){ console.log(1); await new Promise((_res,_rej)=>_res(1)); console.log(2); }
如果await后面是一个Promise,则这个Promise对象的终态会触发后续代码的执行。换句话说await 语句之后的代码是await的这个Promise对象的then逻辑
这里在老版本Chrome中可能会不一致,原因是新版中进行了激进优化 :
await v
在语义上将等价于Promise.resolve(v)
,而不再是老版本的new Promise(resolve => resolve(v))tsfunction async1(){ console.log(1); return Promise.resolve(new Promise((_res,_rej)=>_res(1))).then(()=>{ console.log(2); }) }
示例二
ts
Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then(res => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
//处理程序(then的callback)里返回 thenable 对象就会导致增加两个任务入列。
这里就不阐述打印顺序了,主要的问题是下面这个问题
Q3:then回调函数如果返回一个Promise/thenable,会造成什么影响吗
首先,先说做题的结论:如果返回一个Promise实例,则会多2次微任务,如果返回thenable,则多一次微任务
-
如果返回一个Promie实例
如果返回的是一个Promise实例,则底层会调用一个
NewPromiseResolveThenableJobTask
函数去创建一个微任务PromiseResolveThenableJob
,这个微任务的内容是去调用这个实例的then方法,用于将实例的onFuifilled返回值传递给外层的Promise(这里不理解可以翻到最后有参考链接)tsPromise.resolve() .then(() => { console.log(0); return Promise.resolve(4); }) .then(res => { console.log(res); });
例如这个例子,将
Promise.resolve()
返回的Promise记为p0,then(() => {console.log(0);return Promise.resolve(4);})
返回的Promise记为p1,将Promise.resolve(4)
返回的Promise记为p2.首先p1的onfulfilled为一个Promise,这时候会执行ResolvePromise
的Enqueue
代码块,里面会调用NewPromiseResolveThenableJobTask
产生一个微任务,这个微任务的作用就是调用p2的then方法,然后在then的回调中执行p1的resolve函数,将值(4)传递给p1,大致像下面这样tslet promiseResolveThenableJobTask = () => { p2.then((value) => { ReslovePromise(p1, value) }) }
注意:promiseResolveThenableJobTask本身也是一个微任务
如果改动一下
tsPromise.resolve() .then(() => { console.log(0); return Promise.resolve(4).then(res=>{ console.log('新加的') }).then(res=>{ console.log('新加的2') }) }) .then(res => { console.log(res);//这里会变为undefined,因为他的res是'新加的2'对于的then返回的值 }); Promise.resolve().then(() => { console.log(1); }).then(() => { console.log(2); }).then(() => { console.log(3); }).then(() => { console.log(4); }).then(() => { console.log(5); }).then(() => { console.log(6); });
这个新增加的then里面的log会在什么时候打印呢,当我们执行到这个第一个then(新加的)的时候,它对应的promise已经为终态了,按正常逻辑走就行,所以会直接加入微任务队列,会在
0,1
之后打印新加的
,然后打印2
,再打印新加的2
,这时,新加的2
所在的then处理函数就该返回一个Promise了,返回的Promise按照上面NewPromiseResolveThenableJobTask
的流程走,会隔2个微任务,也就是继续打印3,4
,然后再打印undefined
。 -
如果返回一个thenable
thenable:thenable对象指的是具有then方法的对象
thenable = {then: function (resolve, reject) {resolve(42);}};
(注意到这里,这里再webWorker那篇文章也会用到)依旧会被创建一个微任务
PromiseResolveThenableJob
,但是和Promise不同的是,在执行这个Job时,会被直接fulfilled,少了调用then的处理,所以只延时1个时序
示例三
ts
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
return Promise.resolve(1);
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve(2);
})
.then(function () {
console.log('1');
})
.then(function () {
console.log('2');
})
.then(function () {
console.log('3');
})
.then(function () {
console.log('4');
});
console.log('script end');
export {};
ts
(async function () {
return Promise.resolve(1);
})().then(() => {
console.log(1);
});
new Promise(resolve => {
resolve(2);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
});
这里也不分析阐述打印顺序了,主要是下面的一个问题,这里可以和示例一相互比较
Q4:Promise.resolve()
与new Promise((_res,_rej)=>(_res()))
有什么不同?
首先给出结论,它两完全不是同一个东西
- Promise.resolve会看传入参数是否是 Promise 实例。如果是则立刻返回传入参数本身。否则,根据自己的 this(一般用法就是全局变量 Promise)调用 new this((resolve, reject)=>...) 创建新的 Promise-like 对象,也就是说它并不会造成
PromiseResolveThenableJob
的创建 new Promise((_res,_rej)=>(_res()))
,而这个是Promise Resolve Function
,他会看onfulfill的返回值是否是thenable,如果是那么就会入列一个新任务PromiseResolveThenableJob
,这个任务之后就会调用thenable的then,注意非 pending 是立刻入列的,否则又要等 resolve/reject 时才真的创建任务入列,这句话将在Q5使用
现在反过来看async function 和 then 处理函数整体的返回值(promise)
-
首先给出一个
async function
的例子tsasync function async1() { await async2(); console.log('async1 end'); } async function async2() { console.log('async2'); return Promise.resolve(1); } async function async3() { console.log('async2'); } async1(); Promise.resolve() .then(() => { console.log(1); }) .then(() => { console.log(2); }) .then(() => { console.log(3); }) .then(() => { console.log(5); }) .then(() => { console.log(6); });
执行async3和async2会得到不同的顺序,其中相差2个时序,表明async显式的返回一个Promise和隐式的返回一个Promise是有不同的
执行async2
执行async3
接着,如果async都是统一将返回值用Promise.resolve()来包裹,那么我们将async2进行转换一下
ts// async function async2() { // console.log('async2'); // return Promise.resolve(1); // } function async2() { console.log('async2'); return Promise.resolve(Promise.resolve(1)); }
我们再来执行以下async2
会发现和原本的执行顺序不同,并且和async3的执行顺序相同,接着我们用new Promise的方式返回Promise实例
ts// async function async2() { // console.log('async2'); // return Promise.resolve(1); // } function async2() { console.log('async2'); return Promise.resolve(Promise.resolve(1)); }
看一下执行顺序,会发现和原本的async2是一致的(本篇文章中说的等价或一致都是指的执行顺序,至于实际是否底层一致并不在讨论范围之内)
所以我们可以知道,对于async函数显式的返回一个thenable,他是使用
new Promise
的方式去包裹返回值的,对于非thenable,则使用Promise.resolve()包裹 -
then 处理函数整体的返回值(promise)
这里我直接甩李杭帆大佬对于then返回promise之后的处理原话
then 创建新 Promise 实例,其中一个子步骤相当于调用 new Promise((resolve, reject)=>...)
利用new Promise创建一个Promise实例,刚好也和我们的给出的结论对上了,同时也是对Q3的补充
示例四
ts
async function async1() {
console.log('async1 start');
// const async2Promise = async2();
// const awaitPromise = Promise.resolve(async2Promise);
// console.log(awaitPromise === async2Promise); //true
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
return new Promise((res, rej) => {
res(
new Promise((res, rej) => {
res(
new Promise((res, rej) => {
res(1);
})
);
})
);
});
}
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve(2);
})
.then(function p1fn() {
console.log('1');
})
.then(function p2fn() {
console.log('2');
})
.then(function p3fn() {
console.log('3');
})
.then(function p4fn() {
console.log('4');
})
.then(function p4fn() {
console.log('5');
});
我们来分析一下:
第一轮:
- 宏任务:
- 执行
async1
,打印async1 start
,执行async2
,打印async2
,发现3个Promise,(为了便于描述,将最里层Promise记为p0,往外依次为p1,p2,由于显式返回Promise,所以async返回会额外套一个Promise(记为p4))- 按照代码顺序依次执行,首先p0进行了res(1),p0执行完毕,p1的res有了结果,res了一个promise,那么开启额外的微任务
PromiseResolveThenableJob
(记为PromiseResolveThenableJob1
),并将其加入到微任务队列,此时微任务队列为[PromiseResolveThenableJob1
]- 继续执行,p1执行完毕,p2的res有了结果(这个结果不是说确定了终态),res了一个promise,那么开启微任务
PromiseResolveThenableJob
(记为PromiseResolveThenableJob2
),并将其加入到微任务队列中,此时微任务队列为[PromiseResolveThenableJob1
,PromiseResolveThenableJob2
]- 继续执行,p2执行完毕,async2函数执行完毕,返回值明确,p3的res有了结果(这个结果不是说确定了终态),res了一个promise,那么开启微任务
PromiseResolveThenableJob
(记为PromiseResolveThenableJob3
),并将其加入到微任务队列中,此时微任务队列为[PromiseResolveThenableJob1
,PromiseResolveThenableJob2
,PromiseResolveThenableJob3
]- 继续执行,await右边的Promise明确,阻塞后边代码,将其放入到Promise.resolve(p3).then(()=>{阻塞代码}),由于Promise.resolve一个Promise会直接返回这个promise,所以也就是p3.then(()=>{阻塞代码}),由于p3状态未确定,所以先加入缓存队列,async1执行完毕,此时微任务队列为[
PromiseResolveThenableJob1
,PromiseResolveThenableJob2
,PromiseResolveThenableJob3
]- 继续执行(后面5个数字分别对应的Promise为p_1、p_2、p_3、p_4、p_5),打印
promise1
,并将最外层Promise(p_1)变为终态,p1fn
加入微任务队列,此时微任务队列为[PromiseResolveThenableJob1
,PromiseResolveThenableJob2
,PromiseResolveThenableJob3
,p1fn
]- 继续执行,后续三个都由于其对应的promise终态未确定,所以先放入缓存队列,到此第一轮宏任务执行完毕
- 微任务:
- 取出
PromiseResolveThenableJob1
,由于p0终态已经确定,p0.then的处理函数(后续都简称为p(0|1|2|3).then)直接加入微任务队列,此时微任务队列为[PromiseResolveThenableJob2
,PromiseResolveThenableJob3
,p1fn
,p0.then
]- 取出
PromiseResolveThenableJob2
,执行,由于p1的终态还未确定(等到p0.then执行完毕才能确定),所以先放入缓存队列,此时微任务队列为[PromiseResolveThenableJob3
,p1fn
,p0.then
]- 取出
PromiseResolveThenableJob3
,执行,由于p2的终态还未确定(等到p1.then执行完毕才能确定),所以先放入缓存队列,此时微任务队列为[p1fn
,p0.then
]- 取出
p1fn
执行,打印1
,执行完毕,p_2终态确定,p2fn
加入微任务队列,此时微任务队列为[p0.then
,p2fn
]- 取出
p0.then
执行,p1终态确定,p1.then放入微任务队列,此时微任务队列为[p2fn
,p1.then
]- 取出
p2fn
执行,打印2
,执行完毕,p_3终态确定,p3fn
加入微任务队列,此时微任务队列为[p1.then
,p3fn
]- 取出
p1.then
执行,p2终态确定,p2.then放入微任务队列,此时微任务队列为[p3fn
,p2.then
]- 取出
p3fn
执行,打印3
,执行完毕,p_4终态确定,p4fn
加入微任务队列,此时微任务队列为[p2.then
,p4fn
]- 取出
p2.then
执行,p3终态确定,p3.then放入微任务队列,此时微任务队列为[p4fn
,p3.then
]- 取出
p4fn
执行,打印4
,执行完毕,p_5终态确定,p5fn
加入微任务队列,此时微任务队列为[p3.then
,p5fn
]- 取出
p3.then
执行,阻塞的代码开始执行,打印async1 end
,执行完毕,此时微任务队列为[p5fn
]- 取出
p5fn
执行,打印5
,执行完毕,此时微任务队列清空,第一轮事件循环执行完毕总打印顺序:
async1 start->async2->promise1->1->2->3->4->async1 end->5
Q5:在Promie中'递归'resolve(Promise)会怎样?
首先记得上文提到了一句话非 pending 是立刻入列的,否则又要等 resolve/reject 时才真的创建任务入列
-
规则一:对于async函数来说,如果显式返回一个Promise,那么会用
new Promise
包裹 -
规则二:使用
new promise
去resolve一个thenable
会创建额外的微任务PromiseResolveThenableJob
,微任务中会调用内层promise的then去实现值的传递 -
规则三:如果调用then时,promise不是终态,会放入缓存队列中,直到变为终态才放入微任务队列中
-
规则四:如果使用Promise.resolve一个Promise实例,立刻返回其Promise实例本身
-
规则五:await一个Promise,等价于Promise.resolve(Promise)
结合上面5条规则,再看下执行顺序分析应该就没问题了(有点规则怪谈的感觉?)
nextTick+Promise(node)
研究node时会继续补充,这部分会涉及到微任务队列的优先级,一类微任务队列执行完毕之后,才能执行下一类微任务队列,先nextTick类微任务队列,再promise类微任务队列
可以先对比看看这2个示例,为什么前面的示例nextTick会在最后打印,而后面的示例可以插队?
ts
new Promise((resolve, reject) => {
console.log(1);
resolve(2);
})
.then(a => {
process.nextTick(() => {
console.log('我是最后打印');
});
console.log(2);
new Promise((resolve, reject) => {
//内层Promise
console.log(3);
resolve(2);
})
.then(c => {
console.log(4);
})
.then(d => {
console.log(6);
});
})
.then(b => {
console.log(5);
});
ts
process.nextTick(function(){
console.log(7);
});
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
process.nextTick(function(){
console.log(8);
});
参考链接
promise.then 中 return Promise.resolve 后,发生了什么? - 知乎 (zhihu.com)
javascript - promise then 的回调函数是在什么时候进入微任务队列的? - SegmentFault 思否
JS-ES6-Promise/Promise A+规范 · Issue #112 · yaofly2012/note (github.com)
令人费解的 async/await 执行顺序 - 掘金 (juejin.cn)
理解 JavaScript 的 async/await - 边城客栈 - SegmentFault 思否
【V8源码补充篇】从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节 - 掘金 (juejin.cn)
javascript - async/await只是then 的语法糖吗?有什么细微上的不同吗?为什么这两段代码输出不一致? - SegmentFault 思否(@普拉斯强的回答我验证了一下,可能是由于激进优化的原因从而在新版本存在一些问题?)