对嵌套的 Promise 的理解

5,570 阅读7分钟

首先,本文是 Promise 链式调用顺序引发的思考 的实践分析,需配套食用。

该文是个人理解学习的总结,如有错漏,恳请斧正。

提取文中几个关键的论点:

  1. promise 的 then/catch 方法执行后会也返回一个 promise

  2. 当执行 then 方法时,如果前面的 promise 已经是 resolved 状态,则直接将回调放入微任务队列中

  3. then 方法是同步执行的,回调才是异步的

  4. 回调事件根据 promise 状态不同,处理也不同:

    4.1 pending : 回调会储存在 promise 内部,既不会执行,也不放到微任务中

    4.2 resolve : 会遍历之前通过 then 给这个 promise 注册的所有回调,将它们依次放入微任务队列中

  5. then 负责注册回调,不会触发回调,也不会将它推入微任务队列中去,是 resolve 负责将回调推入微任务队列,由事件循环取出并执行

  6. 对于 then 方法返回的 promise 它是没有 resolve 函数的,取而代之只要 then 中回调的代码执行完毕并获得同步返回值,这个 then 返回的 promise 就算被 resolve。同步返回值的意思换句话说,如果 then 中的回调返回了一个 promise,那么 then 返回的 promise 会等待这个 promise 被 resolve 后再 resolve。

第 6 点可能有点绕,其实说的只有一个事情,就是,then 方法只要获取到同步返回值就算是执行完毕,可以进行下一个 then 的执行了, 所以,我们只需要搞清楚同步返回值是什么就可以了。

同步返回值大概有以下两种情况:

  1. 同步代码执行后的返回值/undefined

  2. 返回了一个 Promise

  3. async/await 情况等同于 2

    MDN async/await

    async 函数可能包含 0 个或者多个 await 表达式。await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。

其中第一点,同步代码执行,如果其中有执行到 Promise,会稍微复杂点,下面用代码说明一下:

// P1
new Promise(resolve => {
  console.log('P1 resolve')
  resolve()
}).then(() => {
  console.log('P1 then 1')
  // P2
  new Promise(resolve => {
    console.log('P2 resolve')
    resolve()
  }).then(() => {
    console.log('P2 then 1')
  }).then(() => {
    console.log('P2 then 2')
  })
}).then(() => {
    console.log('P1 then 2')
  })
// P1 resolve 
// P1 then 1
// P2 resolve
// P2 then 1
// 注意:此时 P1 的第一个 then 就算是执行完毕了,下面会执行 P1 的第二个 then
// P1 then 2
// P2 then 2

对于第二点,也用代码说明一下:

// P1
new Promise(resolve => {
  console.log('P1 resolve')
  resolve()
}).then(() => {
  console.log('P1 then 1')
  // P2
  return new Promise(resolve => {
    console.log('P2 resolve')
    resolve()
  }).then(() => {
    console.log('P2 then 1')
  }).then(() => {
    console.log('P2 then 2')
  })
}).then(() => {
    console.log('P1 then 2')
  })
// P1 resolve
// P1 then 1
// P2 resolve
// P2 then 1
// 注意:这里可以看到,跟上个例子不用的地方就在于,这里用了 return ,所以 P1 的第一个 then 执行完毕得等待 P2 完整执行完才算是执行完,才能 return 一个同步返回值回去
// 所以这里的结果跟上个例子不一样了
// P2 then 2
// P1 then 2

第三点也用代码说明一下:

new Promise(resolve => {
  console.log('P1 resolve')
  resolve()
}).then(async () => {
  console.log('P1 then 1')
  // P2
  // 也等同于 return await P2
  await new Promise(resolve => {
    console.log('P2 resolve')
    resolve()
  }).then(() => {
    console.log('P2 then 1')
  }).then(() => {
    console.log('P2 then 2')
  })
}).then(() => {
    console.log('P1 then 2')
  })
// P1 resolve
// P1 then 1
// await 表达式会暂停整个 async 函数的执行进程并出让其控制权,只有当其等待的基于 promise 的异步操作被兑现或被拒绝之后才会恢复进程。promise 的解决值会被当作该 await 表达式的返回值。所以 P2 会被完整执行完毕后,当做 await 表达式的返回值,如果 await 后没有其他的代码的话(如果有,将会继续执行),不管是否 return ,此时都相当于执行完同步代码了,也就是 P1 的 then 执行完毕了
// P2 resolve
// P2 then 1
// P2 then 2
// P1 then 2

以下是实践代码:

// 简称 p1
new Promise(resolve => {
  console.log('a1');
  resolve();
})
.then(async () => {
  console.log('a2')
  // 简称 p2
  new Promise(resolve => {
    console.log('b1');
    resolve();
  })
  .then(() => console.log('b2'))
  .then(() => console.log('b3'))
  .then(() => console.log('b4'))
})
.then(() => console.log('a3'))
.then(() => console.log('a4'))
// a1 a2 b1 b2 a3 b3 a4 b4

对结果中的 a1 a2 b1 b2 的分析如下:

  1. 先执行宏任务 p1,也就是打印 a1,然后 resolve
  2. 根据论点2 和 论点 4.2(强调是依次推入),第一个 then 注册的回调将放到微任务队列中去
  3. 宏任务执行完毕,执行微任务
  4. 取出队列中的第一个事件,也就是第二点的 then 注册的回调事件,执行,打印 a2,然后执行同步代码进入 promoise(p2) 中去,打印 b1,此后,执行 p2 的 resolve,将 p2 的第一个 then 注册的回调事件推入微任务中。
  5. 微任务执行完毕,然后检查此次微任务是否创建了微任务,有则执行,于是取出 p2 的第一个 then 注册的回调事件执行,打印 b2
  6. 执行微任务时创建的微任务执行完毕。

来到了命运的转折点,分析后面为何依次打印 a3 b3 a4 b4:

  1. 第6点后,也就是 p2 的第一个 then 执行完毕,根据论点6,此时 p1 的 第一个 then 已经被 resolve 了
  2. 根据论点2 和 论点 4.2,将 p1 的第二个 then 注册的回调事件推入微任务队列
  3. 根据论点6,p2 的第一个 then resolve 了,此时它会将 第二 then 推入微任务队列中
  4. 微任务队列开始执行,取出第一个任务,也就是 p1 的第二个 then 注册的回调事件,执行,打印 a3,任务执行完毕
  5. 取出第二个任务,也就是 p2 的第二个 then 注册的回调事件,执行,打印 b3,任务执行完毕
  6. 重复 7 - 11,直到微任务队列为空

于是 a3 b3 a4 b4 就是这样依次打印啦。

But

遇事不决,先杠一下(千万别学):

为什么 p2 的第一个 then resolve 后,不先将跟随它后面的 第二个 then 注册的回调事件推入微任务,然后再将 p1 的第二个 then 推入微任务中。也就是结果打印是 b3 a3?

此处我觉得不要被代码的顺序影响。

因为 p1 的第一个 then 注册的回调事件先执行,那么当 resolve 后,理应也是第一个被处理,然后再到 p2 第一个 then 注册的回调事件的 resolve 后的处理。

最后来个有意思(也常见)的问题,请看代码:

第一个例子:

// 代码来自 [Promise 链](https://zh.javascript.info/promise-chaining)
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

第二个例子:

// 代码来自 [Promise 链](https://zh.javascript.info/promise-chaining)
new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000); 
})
.then(function(result) {
  alert(result); 
  return result * 2;
})
.then(function(result) { 
  alert(result); 
  return result * 2;
})
.then(function(result) {
  alert(result); 
  return result * 2;
});

从运行结果来看,可以看出 可以将多个 .then 添加到一个 promise 上。但这并不是 promise 链(chaining)。

为什么呢?

这个可以用论点1来回答。

因为 then 会返回一个 promise(pNext),下一个 then 其实是挂在 pNext 上的。这样当上一个 then 完成后,会将下一个 then 的回调事件推入微任务队列中。且下一个 then 的参数是上一个 then 返回的。这样 promise 就串起来了。

结合代码解释一下:

第一个例子中 promise 被赋值后,接下来继续往下走,你会发现,三个 then 都是挂在 promise 上的 ,等待 promise resolve 后,这三个回调事件均会被推入微任务队列中,所以它们会一起被执行,且得到的参数也是 promise resolve 的值。所以 alert 的结果都是 1。

第二个例子的情况,可以看到,下一个 then 是挂在上一个 then 返回的 promise 上(论点1)。所以下一个 then 拿到的值是上一个 then 传的,所以 alert 的值是 1,2,4。

参考

Promise 链式调用顺序引发的思考

Promise 链