关于Promise的异步执行顺序理解

Posted Pige

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Promise的异步执行顺序理解相关的知识,希望对你有一定的参考价值。

Promise

假设你已经对Promise的使用相当熟悉,假设你对它的实现原理相当熟悉,假设你已经写过很多次Promise的实现相当熟悉,那么开始往下撸。

浏览器:谷歌 84.0.4147.135(正式版本) (64 位)

若是在不同浏览器存在不一样的结果,那你赚到了。


主要是一些执行顺序的总结,避免你被坑。

首先考虑一个问题:promise.then() 是异步任务么?

必然是一个异步任务。它的作用就是对传入的回调函数进行处理。原理与 setTimeout 类似,setTimeout 会将回调加入到异步任务队列,待到下一轮事件循环开始时调用。而 then 对回调的处理比较复杂,因为不仅需要依赖promise的状态,还依赖 resolve 的调用时机,就像 setTimeout需要依赖 timeout 一样。

总结

  1. 当所有 promise 对象的状态是非pending时,按照 then 的定义顺序执行 then 的回调
  2. 当所有 promise 对象的状态是pending时,按照 resolve 函数的执行顺序执行 then 的回调(即使早就定义好了 then 的回调结果也按照 resolve 的执行顺序执行)
  3. 当同时存在上面的两种情况时,需要结合上面两种情形来判断
  4. 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. 执行顺序是 1 2 3 4 5 6
  2. 这里的 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 代表执行顺序
  1. 执行 a 步后,返回三个 fulfilled 状态的 promise 对象,执行 b 步后按照顺序加入了then,在执行 c 步前,这里就是【总结一】的实现。【重点】是调用 then 方法之后,会返回一个 pending 状态的 promise 对象,因此执行 b 步后,pp1 pp2 pp3 是 pending 状态。
  2. 执行 c 步时,pp1 pp2 pp3 还是 pending 状态,因此 Promise 会把 then 的回调暂时保存到【缓存】中(这里还没有加入到异步队列),等到 b 步中 then 的回调被执行时,在这个回调里会执行【返回的 pending 状态的对象】(即 pp1 pp2 pp3) 的 resolve 函数,调用 resolve 后【缓存】的回调才会被加入到异步队列中
  3. 分析上面的执行,从上往下同步执行代码,先输出 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); 
})
  1. 结合【总结一】,到这里的时候执行顺序是 123564,在 c 步中的顺便依旧保持不变, pp1是先定义,其次是 pp2 pp3。
  2. 按照【总结一】此时在 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 的调用位置。
  1. 结合【总结一】,7 8 两个必然是先执输出 7 再输出 8,只是中间可能会包含 5 6。而 5 6 的实际位置就要根据 resolve1 和 resolve2 的位置来定。
  2. 情况一:resolve1 和 resolve2 位置相连
    在 a b c 执行,7 8 前面输出 5 6,无论谁先执行,最终结果都是 12345678
    在 d 执行时,7 8 中间包含 5 6,resolve1 resolve2 谁先执行谁先输出
    在 e 执行时,7 8 后面输出 5 6,resolve1 resolve2 谁先执行谁先输出
  3. 情况二: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。

  4. 总结就是,代码顺序执行的过程中,遇到是非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)
});
  1. p1 是一个 fulfilled 状态的 promise 对象 Promise {<fulfilled>: 1} ,证明内部没有对这个 p 进行处理
  2. 按照前面总结,相信你就能轻松的知道执行顺序,实际执行结果 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)
});
  1. p1 是一个pending状态的 promise 对象 Promise {<pending>} ,证明其内部有对 p 进行处理,而且未真正调用 resolve 函数。
  2. 实际执行结果 123578,少了输出 4 和 6,这就意味着 resolve 在处理 promise 对象作为参数时,在内部会执行两次【异步】。
  3. 参数 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);
});
  1. 按照前面的总结,假设的执行顺序应该是 1234578,实际执行结果是 1234578,对比发现结果是一致的,跟上个例子对比多了输出4,跟第一个例子比少了输出6,这就意味着 resolve 在处理 promise 对象作为参数时,在内部会执行一次【异步】。
  2. 把上个例子和这个例子结合对比,同是 promise 对象,但是处理方式不一样,是不是矛盾了?其实不是,仔细观察你会发现两个例子中,p 的状态是不一样的。
  3. 参数 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
  1. p1 是一个pending状态的 promise 对象 Promise {<pending>} ,证明其内部有对 p 进行处理,而且未真正调用 resolve 函数。
  2. 看控制台输出”我是thenable的then方法“在 3 之后 5 之前输出,实际执行结果是 1 2 3 我是thenable的then方法 5 6 7 8。对比可以发现,原来的输出 4 的位置变成了 “我是thenable的then方法”,也就是说在使用 resolve 传递 【带then的对象时】时内部执行了一次【异步】。
  3. 参照前三个总结,可以知道 a 的回调肯定是第一个压入到异步队列中的,其次执行 2 时把带 then 方法的对象传进去,2 内部【并未执行真正的 resolve】只是对这个对象进行了一次【异步】(暂且称为 d,我猜测是使用了类似 Promise.resolve() 的逻辑)处理,这个 d 的回调是第二个压入异步队列。根据【总结二】知道 b c 的回调只是暂时保存到【缓存】,等到 a d 的回调执行时就会把 b c 的回调【缓存】依次加到异步队列中

五、扩展

假设你已经对 async/await 的使用相当熟悉,下面讲的是 await 会涉及到一些参数问题。分为四大类:

  1. 原始类型和不带then的引用类型
  2. promise对象
  3. 带then的对象
  4. 异步函数
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. 执行顺序是1 2 3 4 5 6
  2. 看执行结果知道遇到 await 之后,异步函数内部会交出使用权,等到同步执行完成就会回到原来的位置继续往下执行,后续就会变成异步执行代码。
  3. 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);
});
  1. 执行顺序 123456,与 1 的结果是一样的,这是不是就意味着对于 1 2这两种参数类型的处理是一样的?
  2. Promise.resolve() 有幂等性, 1和2.1这两种类型处理结果最终返回一个 fulfilled 状态的 promise 对象。我【个人认为】它的内部就是经过类似 Promise.resolve() 功能处理,处理完之后加入一个【异步】,并在这个【异步】执行时返回 await 等待的结果。
  3. 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. 执行结果是 1 2 3 0 5 4 6。此时的参数是一个 pending 状态,并在执行 await 的时候就把输出 0 的回调(暂称为 a)加入到异步队列中,await 内部的【异步】等到这个 a 执行后才加入异步队列。
  2. 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. 执行顺序是 1 2 3 4 5 6
  2. 根据执行结果分析。首先执行 async1() 时,输出 1,遇到 await 时对参数 1 进行处理,这里会有一个【异步A】(称为 a,这个异步我认为与前文定义的异步是有差异的,至少它会多一个标识,用来证明它正在执行async/await操作【仅仅是个人想法】)被压入到异步队列中,同时交出使用权。其次执行 async2() 时,输出 2,然后 then 的回调(称为 b)压入到异步队列中 。
  3. async1() 和 async2() 调用后返回的是一个 pending 状态的 promise 对象,后面的 then 的回调会被保存到【缓存】中,然后按照【总结二】执行。
  4. 到这整个同步执行结束,开始执行异步任务。首先执行异步 a,拿到执行结果后,根据携带标识唤醒 async1,并把值返回,然后执行 async1 后续代码输出 3,内部代码执行完毕,【重点】是异步函数内部会判断是否有return,没有就 return undefined,同时执行 async1() 返回的 promise 对象的 resolve 函数,把输出 5【缓存】的回调加入到异步队列,至此结束当前异步任务。
  5. 其次执行异步 b,按照【总结一】执行输出 4。最后把输出 6【缓存】的回调加入到异步队列,至此结束当前异步任务。
  6. 到这来已经转变成了 promise 的相关执行顺序,最后依次输出 5 6 。
  7. 互换 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. 执行结果 1 2 3 0 5 4 6 ,执行结果与 2.2 是一样的。
  2. 其中 0 是有输出的,就证明这一步经过 await 处理后会变成一个【异步】,再通过【异步A】处理返回值。这是为什么呢?
  3. 不妨加入 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. 执行结果 1 2 3 4 5 6,与 2.1 结果一致。
  2. 首先需要了解异步函数返回的实质是什么?是一个 promise 对象,什么状态取决于当前函数体内是否包含 await,有则是 pending 状态,否则是 fulfilled 状态(这里不讨论抛出错误的情况)。
  3. 判断 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的异步执行顺序理解的主要内容,如果未能解决你的问题,请参考以下文章

promise执行顺序

关于异步执行顺序

JavaScript——异步操作以及Promise 的使用

关于Promise

关于多个Promise对象及then()函数的执行顺序的研究记录

关于Promise详解