promise.finally为什么在其调用者的then后面执行?

const a = () => {
  return new Promise((resolve) => {
    resolve(1);
  });
};

const b = () => {
  return new Promise((resolve) => {
    a()
      .then((aR) => {
        console.log(aR);
        resolve(2);
      })
      .finally(() => {
        console.log("A");
      });
  });
};

const c = () => {
  b().then((bR) => {
    console.log(bR);
  });
};

c();

今天有一段代码抽象后逻辑如上述代码,我本来觉得输出顺序应该是

1
A
2

但实际输出却是

1
2
A

不太理解是为什么,求指教。


环境:Chrome 版本 126.0.6478.115(正式版本) (64 位)


2024.6.24 14:50 更新:

首先感谢各位的答疑解惑,受益匪浅。

在上面的代码中,我觉得输出 1 A 2 是认为,当c中运行 b() 时调用了函数bb中运行了a,并进入then逻辑输出1then完毕后执行finally输出A,此时b()运行完毕,输出b的返回结果2,所以顺序是 1 A 2

这里思考的逻辑我参考的是Java中的try catch finally。但现在我发现这本质上有所区别,应该结合浏览器的微队列,以及Promise的then与finally都会返回一个Promise对象并将其放到微队列中等待执行,其执行顺序则与放入微队列的先后顺序有关。

b中,当a()运行得到返回结果1后,进入then逻辑输出了1,并返回了2,此时其实已经将结果封装为Promise对象并放入微队列中,后续finally的内容虽然也会封装为Promise对象并放入微队列,但并不会对前者造成影响。
所以此时在c中调用b()并得到结果时,就会立即运行后续代码,而不会等待finally的执行,因此实际运行时才会输出 1 2 A

当然,我现在也是对着答案想原因,下次遇到也不知道能不能在看到答案前想明白。

最后,再次感谢各位。

阅读 1.2k
avatarAI BotBETA

Promise.prototype.finally() 方法返回一个 Promise,并且在 promise 执行结束时,无论其状态如何,都会执行指定的回调函数。这个方法的回调函数会在当前 Promise 链的最后执行,但它不会改变 Promise 的结果或者状态。

在你提供的代码中,finally 是在 a().then(...) 链的末尾被调用的,但是它不会阻塞或改变 a().then(...) 链的异步执行流程。finally 仅仅确保,无论 a().then(...) 的结果如何,它内部的回调最终都会被调用,但是它并不改变链中其他 then 的执行顺序。

b() 函数返回一个新的 Promise,这个 Promise 在 a() 完成并解析其值后解析为 2a() 中的 then 回调首先执行,然后 finally 回调执行。b() 返回的 Promise 在 a()finally 回调执行完毕后解析为 2

c() 调用 b() 并附加一个 then 回调时,这个回调会在 b() 返回的 Promise 解析为 2 时执行。由于 finally 是在 b() 内部的 Promise 链的末尾,所以它的回调会在 b() 返回的 Promise 解析之前执行。

输出顺序为:

1 // a().then((aR) => { console.log(aR); ... } 中的 console.log
2 // b().then((bR) => { console.log(bR); } 中的 console.log
A // a().finally(() => { console.log("A"); }) 中的 console.log

finally 回调在 a().then(...) 链的末尾执行,但是它不影响 b() 返回的 Promise 的解析顺序。因此,在 c()then 回调中打印 2 的操作会在 finally 回调打印 "A" 之前发生。

简单来说,finally 回调执行的时间点是在当前 Promise 链的末尾,但在返回的 Promise 解析之前。在这个例子中,b() 返回一个 Promise,这个 Promise 在 a()finally 回调执行后解析,所以在 c()then 回调中,2 会先被打印,然后是 a()finally 回调中的 "A"

3 个回答

(以下的 queue 都指 microtask queue)

注意到

    a()
      .then((aR) => {
        console.log(aR);
        resolve(2);
      })
      .finally(() => {
        console.log("A");
      });

这是链式调用a().then(...) 返回一个 Promise,等这个 Promise fulfill 才会输出 A,而不是 a() fulfill 之后。这样的话 resolve(2) 会先一步把 b().then 放入 queue,然后才 resolve a().then,然后再把这个 a().finally 放入 queue

写成这样会明白很多:

const f = () => {
  const a = new Promise((resolve) => {
    const cb1 = (result1) => {
      console.log(result1)
      resolve(2)
    }
    const cbA = () => console.log('A')
    const b = Promise.resolve(1)
    const c = b.then(cb1)
    c.finally(cbA)
  })
  return a
}

const cb2 = (result2) => console.log(result2)
f()
  .then(cb2)

执行过程是这样的:

  1. 开始执行 f
  2. 开始执行 a
  3. b 立即被 fulfill,导致 cb1 进入 queue,现在 queue = [cb1]
  4. c 进入 queue
  5. f 返回
  6. 开始执行 queue 中的函数 cb1,现在 queue = []
  7. 输出 1,resolve a,导致 cb2 进入 queue,现在 queue = [cb2]
  8. cb1 执行结束,导致 c 被 resolve,导致 cbA 进入 queue,现在 queue = [cb2, cbA]
  9. 开始执行 queue 中的函数 cb2,现在 queue = [cbA]
  10. 输出 2
  11. 开始执行 queue 中的函数 cbA,现在 queue = []
  12. 输出 A,当前 tick 结束

这个和入栈顺序有关系。你可以在VSC里面用调试模式看到这个具体函数执行和微任务入栈过程。

看看 Promise.prototype.finally() - JavaScript | MDN (mozilla.org)

finally 设计的本意就是在 then 或 catch 之后调用,不管是 resolve 还是 reject 都会触发,避免在 then 和 catch 中写重复代码。所以

a().then().finally() 就是先 then 再 finally 啊。


用 async/await 的方式来写这段代码,就可以看到类似 try ... catch 的逻辑

const a = async () => {
    return await Promise.resolve(1);
};

const b = async () => {
    try {
        const aR = await a();
        console.log(aR);
        console.log(2);
    } finally {
        console.log("A");
    }
};

const c = async () => {
    const bR = await b();
    console.log(bR);
};

c();
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏