关于Promise的异步执行顺序理解
Posted Pige
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Promise的异步执行顺序理解相关的知识,希望对你有一定的参考价值。
Promise
假设你已经对Promise的使用相当熟悉,假设你对它的实现原理相当熟悉,假设你已经写过很多次Promise的实现相当熟悉,那么开始往下撸。
浏览器:谷歌 84.0.4147.135(正式版本) (64 位)
若是在不同浏览器存在不一样的结果,那你赚到了。
主要是一些执行顺序的总结,避免你被坑。
首先考虑一个问题:promise.then() 是异步任务么?
必然是一个异步任务。它的作用就是对传入的回调函数进行处理。原理与 setTimeout 类似,setTimeout 会将回调加入到异步任务队列,待到下一轮事件循环开始时调用。而 then 对回调的处理比较复杂,因为不仅需要依赖promise的状态,还依赖 resolve 的调用时机,就像 setTimeout需要依赖 timeout 一样。
总结
- 当所有 promise 对象的状态是
非pending
时,按照 then 的定义顺序执行 then 的回调 - 当所有 promise 对象的状态是
pending
时,按照 resolve 函数的执行顺序执行 then 的回调(即使早就定义好了 then 的回调结果也按照 resolve 的执行顺序执行) - 当同时存在上面的两种情况时,需要结合上面两种情形来判断
- resolve可以接受不同参数,分为三类,不then方法的任何类型、带then方法的对象和promise对象,根据不同参数类型会有不同的处理方式
一、当所有 promise 对象的状态是非 pending 时,按照 then 的定义顺序执行 then 的回调
let p1 = new Promise(resolve=> {
console.log(1);
resolve()
})
let p2 = new Promise(resolve=> {
console.log(2);
resolve()
})
let p3 = new Promise(resolve=> {
console.log(3);
resolve()
})
p1.then(()=>{
console.log(4);
})
p2.then(()=>{
console.log(5);
})
p3.then(()=>{
console.log(6);
})
- 执行顺序是 1 2 3 4 5 6
- 这里的 p1 p2 p3 在初始化结束后全部是 fulfilled 状态,此时调用 then 方法时, Promise 会把 then 的回调直接压入到异步队列的栈底,按照谁先进入谁先执行(“先进先出”)的顺序执行。假设把 p1.then 放到末尾,then 变成了最后定义,此时的执行结果变成 1 2 3 5 6 4
二、当所有 promise 对象的状态是 pending 时,按照 resolve 函数的执行顺序执行 then 的回调(即使早就定义好了 then 的回调结果也按照 resolve 的执行顺序执行)
// a
let p1 = new Promise(resolve=> {
console.log(1);
resolve()
});
let p2 = new Promise(resolve=> {
console.log(2);
resolve()
});
let p3 = new Promise(resolve=> {
console.log(3);
resolve()
})
// b
let pp1 = p1.then(()=>{
console.log(4);
})
let pp2 = p2.then(()=>{
console.log(5);
})
let pp3 = p3.then(()=>{
console.log(6);
})
// c
pp1.then(()=>{
console.log(7);
})
pp2.then(()=>{
console.log(8);
})
pp3.then(()=>{
console.log(9);
})
// a b c 代表执行顺序
- 执行 a 步后,返回三个 fulfilled 状态的 promise 对象,执行 b 步后按照顺序加入了then,在执行 c 步前,这里就是【总结一】的实现。【重点】是调用 then 方法之后,会返回一个 pending 状态的 promise 对象,因此执行 b 步后,pp1 pp2 pp3 是 pending 状态。
- 执行 c 步时,pp1 pp2 pp3 还是 pending 状态,因此 Promise 会把 then 的回调暂时保存到【缓存】中(这里还没有加入到异步队列),等到 b 步中 then 的回调被执行时,在这个回调里会执行【返回的 pending 状态的对象】(即 pp1 pp2 pp3) 的 resolve 函数,调用 resolve 后【缓存】的回调才会被加入到异步队列中
- 分析上面的执行,从上往下同步执行代码,先输出 1 2 3,然后 4 5 6 的回调是第一批加入到异步队列中,7 8 9 的回调暂时先【缓存】,到这里整个同步代码执行结束;下一步执行异步任务,也就是执行 4 5 6 的回调,同时会调用【返回的 pending 状态的对象】 的 resolve 函数,然后依次把【缓存】的回调加入异步队列中,上面是先执行了 p1.then 的回调,因此 pp1.then 【缓存】的回调优先加入异步队列;最后 4 5 6的回调执行结束,会继续检查异步队列中是否存在任务,发现有7 8 9 的回调任务,因此继续执行,直到异步队列没有任务
// 把上面 b 步的 pp1 放到最后,看看结果是不是按照第三点描述的那样实现
let pp2 = p2.then(()=>{
console.log(5);
})
let pp3 = p3.then(()=>{
console.log(6);
})
let pp1 = p1.then(()=>{
console.log(4);
})
- 结合【总结一】,到这里的时候执行顺序是 123564,在 c 步中的顺便依旧保持不变, pp1是先定义,其次是 pp2 pp3。
- 按照【总结一】此时在 b 步中 then 的回调的执行顺序变成 p2 p3 p1。按照上面第3点分析,对应的调用【返回的 pending 状态的对象】的 resolve 函数顺序变成 pp2 pp3 pp1,【缓存】的回调加入异步队列的顺序也就变成了 pp2 pp3 pp1,从这里看在 promise 对象还是 pending 状态时,then 的回调加入顺序与定义无关,与 resolve 函数【执行】有关,因此可以概况为谁先调用 resolve 谁先执行回调
三、当同时存在上面的两种情况时,需要结合上面两种情形来判断
let resolve1;
let resolve2;
let p1 = new Promise(resolve=> {
console.log(1);
resolve1 = resolve;
});
let p2 = new Promise(resolve=> {
console.log(2);
resolve2 = resolve;
});
let p3 = new Promise(resolve=> {
console.log(3);
resolve()
})
let p4 = new Promise(resolve=> {
console.log(4);
resolve()
})
// a
p1.then(()=>{
console.log(5);
})
// b
p2.then(()=>{
console.log(6);
})
// c
p3.then(()=>{
console.log(7);
})
// d
p4.then(()=>{
console.log(8);
})
// e
// p1 p2 是pending状态, p3 p4 是 fulfilled 状态。a b c d e 是准备给 resolve1 和 resolve2 的调用位置。
- 结合【总结一】,7 8 两个必然是先执输出 7 再输出 8,只是中间可能会包含 5 6。而 5 6 的实际位置就要根据 resolve1 和 resolve2 的位置来定。
- 情况一:resolve1 和 resolve2 位置相连
在 a b c 执行,7 8 前面输出 5 6,无论谁先执行,最终结果都是 12345678
在 d 执行时,7 8 中间包含 5 6,resolve1 resolve2 谁先执行谁先输出
在 e 执行时,7 8 后面输出 5 6,resolve1 resolve2 谁先执行谁先输出情况二:resolve1 和 resolve2 位置不相连,这里情况太多了,主要举例两个
(1)resolve2 在 a 执行,resolve1 在 e 执行,结果是12346785。首先是定义 p1.then() 时 p1 还是pending状态,按照【总结二】会把回调存到【缓存】中,其次是执行定义 p2.then() 之前 resolve2 已经执行,p2 状态改变成 fulfilled,因此 p2 按照【总结一】执行,然后是 p3 p4 也是按照【总结一】执行,最后执行 resolve1 时再把执行 p1.then 的【缓存】回调加入到异步队列中,最后顺序是 p2 p3 p4 p1。
(2) resolve1 在 c 执行,resolve2 在 d 执行,结果是 12345768。首先 p1 p2 按照【总结二】先定义了 then,会把回调存在【缓存】,然后执行 resolve1,按照【总结二】把 p1 的【缓存】回调加入到异步队列,接着按照【总结一】执行 p3.then(),再接着执行 resolve2,按照【总结二】把 p2 的【缓存】回调加入到异步队列,最后执行按照【总结一】执行 p4.then(),最终顺序是 p1 p3 p2 p4。
- 总结就是,代码顺序执行的过程中,遇到是非pending状态的 promise 对象,直接把回调加到异步队列,遇到pending状态的 promise 对象时,先有定义then的情况先把回调【缓存】起来,等到真正执行 resolve 时,再把【缓存】的回调加到异步队列,而且这两种情况是存在【穿插执行】
四、resolve可以接受不同参数,分为三类,不then方法的任何类型、带then方法的对象和promise对象,根据不同参数类型会有不同的处理方式
1. 了解 finally
// 先来一个简单的例子
new Promise(resolve => {
console.log(1);
resolve()
}).then(()=>{
console.log(3);
}).then(()=>{
console.log(5);
}).then(()=>{
console.log(7);
}).then(()=>{
console.log(9);
})
new Promise(resolve => {
console.log(2);
resolve()
}).then(()=>{
console.log(4);
}).then(()=>{
console.log(6);
}).then(()=>{
console.log(8);
}).then(()=>{
console.log(10);
})
// 结合前面的总结很容易就知道执行顺序是 1 2 3 4 5 6 7 8 9 10
1.1 情况一
// 情况一:去掉输出8的then,然后输出6的then替换成finally
new Promise(resolve => {
console.log(1);
resolve()
}).then(()=>{
console.log(3);
}).then(()=>{
console.log(5);
}).then(()=>{
console.log(7);
}).then(()=>{
console.log(9);
})
new Promise(resolve => {
console.log(2);
resolve()
}).then(()=>{
console.log(4);
}).finally(()=>{
console.log(6);
}).then(()=>{
console.log(10);
})
// 实际执行顺序是 1 2 3 4 5 6 7 9 10
1.2 情况二
// 情况二:去掉输出6的then和输出8的then,然后输出4的then替换成finally
new Promise(resolve => {
console.log(1);
resolve()
}).then(()=>{
console.log(3);
}).then(()=>{
console.log(5);
}).then(()=>{
console.log(7);
}).then(()=>{
console.log(9);
})
new Promise(resolve => {
console.log(2);
resolve()
}).finally(()=>{
console.log(4);
}).then(()=>{
console.log(10);
})
// 执行顺序按数字 1 2 3 4 5 7 9 10
对比两次修改,不难发现,第一次修改后,少了 8,第二次修改后少了 6 和 8。共同点就是两次都给都使用了finally,不同点也是下面将会用到的【重点】,那就是调用 finally 时两个对象的状态是不一样的,第一次修改时调用的是一个 pending 状态的对象,第二次是一个 fulfilled 状态的对象。也就是说按照不同的状态对象调用 finally,其内部的实现也存在着不一样。pending 状态时内部执行一次【异步】(这里我也不知道内部是怎样执行的,所有把暂时这个操作称为异步,下面会继续用到),fulfilled 状态
时内部实现两次【异步】。
2. resolve 参数为不带then方法的任何类型
// resolve 参数为不带then方法的任何类型(即五种原始类型和不带then的引用类型),这个类型 resolve 不做任何处理,会直接返回
new Promise(resolve => {
console.log(1);
resolve()
}).then(()=>{
console.log(3);
}).then(()=>{
console.log(5);
}).then(()=>{
console.log(7);
});
let p = 1; // null boolean number object function undefined string
new Promise(resolve => {
console.log(2);
resolve(p)
}).then(()=>{
console.log(4);
}).then(()=>{
console.log(6);
}).then(()=>{
console.log(8);
});
let p1 = new Promise(resolve => {
resolve(p)
});
- p1 是一个 fulfilled 状态的 promise 对象 Promise {<fulfilled>: 1} ,证明内部没有对这个 p 进行处理
- 按照前面总结,相信你就能轻松的知道执行顺序,实际执行结果 1 2 3 4 5 6 7 8
3. resolve 参数为promise对象
3.1 当 p 的状态是 fulfilled 时
// resolve 参数为promise对象,这个类型 resolve 内部会做两次【异步】处理
new Promise(resolve => {
console.log(1);
resolve()
}).then(()=>{
console.log(3);
}).then(()=>{
console.log(5);
}).then(()=>{
console.log(7);
});
let p = new Promise(resolve => {
resolve()
});
new Promise(resolve => {
console.log(2);
resolve(p)
}).then(()=>{
console.log(8);
});
let p1 = new Promise(resolve => {
resolve(p)
});
- p1 是一个pending状态的 promise 对象 Promise {<pending>} ,证明其内部有对 p 进行处理,而且未真正调用 resolve 函数。
- 实际执行结果 123578,少了输出 4 和 6,这就意味着 resolve 在处理 promise 对象作为参数时,在内部会执行两次【异步】。
- 参数 p 当前状态是 fulfilled,看执行结果就知道 resolve 在处理 promise 对象为参数时的过程会在内部调用两次【异步】,情况与上面的 finally 的情况二例子有点相似,因为调用 finally 时内部同样会调用两次【异步】,至于 resolve 内部是不是真的使用了 finally的内部处理呢?我认为不是,但至少是类似的。(因为看不到Promise的实现源码,而且 finally 是 ES2018 引入,Promise 是 ES2015 引入)。
3.2 当 p 的状态是 pending 时
// 假设:按照 finally 逻辑实现的处理,p 作为 pending 状态的对象传入
// 在原来的基础上进行修改,新增输出 4
new Promise(resolve => {
console.log(1);
resolve()
}).then(()=>{
console.log(3);
}).then(()=>{
console.log(5);
}).then(()=>{
console.log(7);
});
let p = new Promise(resolve => {
resolve()
}).then(()=>{
console.log(4);
});
new Promise(resolve => {
console.log(2);
resolve(p)
}).then(()=>{
console.log(8);
});
- 按照前面的总结,假设的执行顺序应该是 1234578,实际执行结果是 1234578,对比发现结果是一致的,跟上个例子对比多了输出4,跟第一个例子比少了输出6,这就意味着 resolve 在处理 promise 对象作为参数时,在内部会执行一次【异步】。
- 把上个例子和这个例子结合对比,同是 promise 对象,但是处理方式不一样,是不是矛盾了?其实不是,仔细观察你会发现两个例子中,p 的状态是不一样的。
- 参数 p 当前状态是 pending,看执行结果 resolve 在处理 promise 对象为参数时的过程会在内部只调用一次【异步】,情况与上面的 finally 的情况一例子相似。
4. resolve 参数为带then方法的对象
// resolve 参数为带then方法的对象(即带then的对象),这个类型在 resolve 内部会做一次【异步】处理
new Promise(resolve => {
console.log(1);
resolve() // 1
}).then(()=>{ // a
console.log(3);
}).then(()=>{ // b
console.log(5);
}).then(()=>{
console.log(7);
});
let p = {
then(resolve, reject) {
console.log(\'我是thenable的then方法\');
resolve();
}
};
new Promise(resolve => {
console.log(2);
resolve(p) // 2
}).then(()=>{ // c
console.log(6);
}).then(()=>{
console.log(8);
});
let p1 = new Promise(resolve => {
resolve(p)
});
// a b c 分别代表单个 then, 1 2 代表两个 resolve
- p1 是一个pending状态的 promise 对象 Promise {<pending>} ,证明其内部有对 p 进行处理,而且未真正调用 resolve 函数。
- 看控制台输出”我是thenable的then方法“在 3 之后 5 之前输出,实际执行结果是 1 2 3 我是thenable的then方法 5 6 7 8。对比可以发现,原来的输出 4 的位置变成了 “我是thenable的then方法”,也就是说在使用 resolve 传递 【带then的对象时】时内部执行了一次【异步】。
- 参照前三个总结,可以知道 a 的回调肯定是第一个压入到异步队列中的,其次执行 2 时把带 then 方法的对象传进去,2 内部【并未执行真正的 resolve】只是对这个对象进行了一次【异步】(暂且称为 d,我猜测是使用了类似 Promise.resolve() 的逻辑)处理,这个 d 的回调是第二个压入异步队列。根据【总结二】知道 b c 的回调只是暂时保存到【缓存】,等到 a d 的回调执行时就会把 b c 的回调【缓存】依次加到异步队列中
五、扩展
假设你已经对 async/await 的使用相当熟悉,下面讲的是 await 会涉及到一些参数问题。分为四大类:
- 原始类型和不带then的引用类型
- promise对象
- 带then的对象
- 异步函数
1. await 参数是原始类型和不带then的引用类型
// 注意:await 后面必须有值,哪怕是 undefined null 或者是 \'\',否则会报错。
async function async1() {
console.log(1);
await 1; // boolean string function array object number undefined null
console.log(3);
}
async function async2() {
console.log(2);
await true;
console.log(4);
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
- 执行顺序是1 2 3 4 5 6
- 看执行结果知道遇到 await 之后,异步函数内部会交出使用权,等到同步执行完成就会回到原来的位置继续往下执行,后续就会变成异步执行代码。
- await 1 先交出的使用权,然后是 await true。等到同步执行完成后,谁先交出谁先恢复。 比如把 async1() 放在 async2() 后调用,执行结果变成 2 1 4 3 6 5
2. await 参数为promise对象
2.1 promise对象状态是 fulfilled 时
async function async1() {
console.log(1);
await 1;
console.log(3);
}
async function async2() {
console.log(2);
await new Promise(resolve => {
resolve()
});
console.log(4);
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
- 执行顺序 123456,与 1 的结果是一样的,这是不是就意味着对于 1 2这两种参数类型的处理是一样的?
- Promise.resolve() 有幂等性, 1和2.1这两种类型处理结果最终返回一个 fulfilled 状态的 promise 对象。我【个人认为】它的内部就是经过类似 Promise.resolve() 功能处理,处理完之后加入一个【异步】,并在这个【异步】执行时返回 await 等待的结果。
- await 的参数换成 Promise.resolve() 后,执行结果是一样的,这也证实了第 2 点。
2.2 promise对象状态是 pending 时
async function async1() {
console.log(1);
await 1;
console.log(3);
}
async function async2() {
console.log(2);
await new Promise(resolve => {
resolve()
}).then(()=>{
console.log(0);
});
console.log(4);
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
- 执行结果是 1 2 3 0 5 4 6。此时的参数是一个 pending 状态,并在执行 await 的时候就把输出 0 的回调(暂称为 a)加入到异步队列中,await 内部的【异步】等到这个 a 执行后才加入异步队列。
- async1 和 async2 比较就是多了一步输出0 的【异步】。
2.3 await 和 promise.then 的优先级谁更高?
async function async1() {
console.log(1);
await 1;
console.log(3);
}
function async2() {
return new Promise(resolve => {
console.log(2);
resolve();
}).then(()=>{
console.log(4);
})
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
- 执行顺序是 1 2 3 4 5 6
- 根据执行结果分析。首先执行 async1() 时,输出 1,遇到 await 时对参数 1 进行处理,这里会有一个【异步A】(称为 a,这个异步我认为与前文定义的异步是有差异的,至少它会多一个标识,用来证明它正在执行async/await操作【仅仅是个人想法】)被压入到异步队列中,同时交出使用权。其次执行 async2() 时,输出 2,然后 then 的回调(称为 b)压入到异步队列中 。
- async1() 和 async2() 调用后返回的是一个 pending 状态的 promise 对象,后面的 then 的回调会被保存到【缓存】中,然后按照【总结二】执行。
- 到这整个同步执行结束,开始执行异步任务。首先执行异步 a,拿到执行结果后,根据携带标识唤醒 async1,并把值返回,然后执行 async1 后续代码输出 3,内部代码执行完毕,【重点】是异步函数内部会判断是否有return,没有就 return undefined,同时执行 async1() 返回的 promise 对象的 resolve 函数,把输出 5【缓存】的回调加入到异步队列,至此结束当前异步任务。
- 其次执行异步 b,按照【总结一】执行输出 4。最后把输出 6【缓存】的回调加入到异步队列,至此结束当前异步任务。
- 到这来已经转变成了 promise 的相关执行顺序,最后依次输出 5 6 。
- 互换 async1() 和 async2() 位置,输出顺序变成 2 1 4 3 6 5。可以看出谁先进入异步队列谁先执行,两个的优先级是一样的。
2.4 对于参数是原始类型和不带then的引用类型的时候,await 内部有没有处理?
在前面例子中已经证明 await 后面的参数是【原始类型和不带then的引用类型的时候】时,内部会同样会对参数进行【异步A】处理
3. await 参数为带then的对象
async function async1() {
console.log(1);
await 1;
console.log(3);
}
class Thenable {
then(resolve){
console.log(0);
resolve();
}
}
async function async2() {
console.log(2);
await new Thenable();
console.log(4);
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
- 执行结果 1 2 3 0 5 4 6 ,执行结果与 2.2 是一样的。
- 其中 0 是有输出的,就证明这一步经过 await 处理后会变成一个【异步】,再通过【异步A】处理返回值。这是为什么呢?
- 不妨加入 2.1 的第 2 点,先使用 Promise.resolve(new Thenable()) 把转换成一个 promise 对象,得到的对象状态是 pending。是不是突然间就感觉是冥冥之中自有安排,这不就是典型的 2.2 的例子。也就是说这两种类型的参数是等效的。
4. await 参数为异步函数
async function async1() {
console.log(1);
await 1;
console.log(3);
}
async function async2() {
console.log(2);
await async3();
console.log(4);
}
async function async3() {
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
- 执行结果 1 2 3 4 5 6,与 2.1 结果一致。
- 首先需要了解异步函数返回的实质是什么?是一个 promise 对象,什么状态取决于当前函数体内是否包含 await,有则是 pending 状态,否则是 fulfilled 状态(这里不讨论抛出错误的情况)。
- 判断 async3() 的返回值,结合 2.1 就能得出相对应的结果。
async function async1() {
console.log(1);
await 1;
console.log(3);
}
async function async2() {
console.log(2);
await async3();
console.log(4);
}
async function async3() {
await 3;
}
async1().then(()=>{
console.log(5);
});
async2().then(()=>{
console.log(6);
});
根据上面第 2 点的判断, async3 返回一个 pending 状态的对象,结合 2.2 得出执行结果是 1 2 3 5 4 6,与 2.2 结果一致。
5. 把异步函数当作 Promise 的 resolve 的参数
在 3 中已经解释过异步函数的返回值本质,因此在 Promise 中使用异步函数当作 resolve 的参数,完全可以按照【三】中 3 的例子进行判断。
以上是关于关于Promise的异步执行顺序理解的主要内容,如果未能解决你的问题,请参考以下文章