为什么 .catch 回调在微任务队列的末尾执行?

Why are .catch callbacks executed at the end of the microtask queue?

我在 HTML 文件中有以下代码:

const fooBar = function(resolve, reject) {
 let flag = (Math.round(Math.random() * 10) % 2);
 if(flag)
  resolve({ "value": "foo", "rand": Math.random() });
 else
  reject({ "value": "bar", "rand": Math.random() });
};
const fooBarSuccess1 = function(value) {
 console.log("Success 1:" + JSON.stringify(value));
};
const fooBarFailure1 = function(value) {
 console.log("Failure 1:" + JSON.stringify(value));
};
const fooBarSuccess2 = function(value) {
 console.log("Success 2:" + JSON.stringify(value));
};
const fooBarFailure2 = function(value) {
 console.log("Failure 2:" + JSON.stringify(value));
};
new Promise(fooBar).then(fooBarSuccess1).catch(fooBarFailure1);
new Promise(fooBar).then(fooBarSuccess2, fooBarFailure2);
console.log("Before setting MicroTask.");
setTimeout(() => console.log("This Timeout was set before the MicroTask!"));
queueMicrotask(() => console.log("From MicroTask!"));
console.log("After setting MicroTask.");

JSFiddle

当 Promise 被拒绝时,fooBarFailure1 在微任务队列的末尾执行,所以你可能会得到以下输出:

Before setting MicroTask.
After setting MicroTask.
Success 2:{"value":"foo","rand":0.3675094508130746}
From MicroTask!
Failure 1:{"value":"bar","rand":0.6828171208953322}
This Timeout was set before the MicroTask!

但是,在queueMicrotask里面的代码执行之前,不应该调用它吗?而且我没有看到 fooBarFailure2 有任何此类问题。它以预期的顺序执行。结果在 Firefox 71 和 Google Chrome 78 中是一样的。有人能解释一下这里发生了什么吗?

不同之处在于 fooBarFailure1fooBarFailure2 离根承诺(来自 new Promise 的承诺)更远。 fooBarFailure1 未连接到根承诺,它连接到由 .then(fooBarSuccess1):

创建的承诺
new Promise(fooBar).then(fooBarSuccess1).catch(fooBarFailure1);

相比之下,fooBarSuccess2fooBarFailure2 附加到根承诺:

new Promise(fooBar).then(fooBarSuccess2, fooBarFailure2);

fooBarFailure1 之前的链中有一个内部拒绝处理程序,但 fooBarFailure2 是直接挂钩的。这就是导致额外异步 "tick".

的原因

让我们只看失败示例,因为它简化了事情:

const success = function(value) {
    console.log("This never happens");
};
const fooBarFailure1 = function(value) {
    console.log("Failure 1");
};
const fooBarFailure2 = function(value) {
    console.log("Failure 2");
};
Promise.reject().then(success).catch(fooBarFailure1);
Promise.reject().then(success, fooBarFailure2);
console.log("Before setting MicroTask.");
setTimeout(() => console.log("This Timeout was set before the MicroTask!"));
queueMicrotask(() => console.log("From MicroTask!"));
console.log("After setting MicroTask.");

其输出为:

Before setting MicroTask.
After setting MicroTask.
Failure 2
From MicroTask!
Failure 1
This Timeout was set before the MicroTask!

原因如下:

  • Promise.reject() returns 在这两种情况下都是被拒绝的承诺。
  • Promise.reject().then(success).catch(fooBarFailure1);
    • .then(success) 创建一个新的承诺并连接履行和拒绝处理程序;拒绝处理程序是内部的,只是传递拒绝原因,因为没有提供拒绝处理程序。
    • .catch(fooBarFailure1) 根据 then.
    • 的承诺连接拒绝处理程序
  • 由于 promise 被拒绝,它使用微任务调用附加到它的拒绝处理程序。
  • Promise.reject().then(success, fooBarFailure2);中:
    • then 将履行处理程序 (success) 和拒绝处理程序 (fooBarFailure2) 连接到来自 Promise.reject()
    • 的承诺
  • 由于 promise 被拒绝,它使用微任务调用附加到它的拒绝处理程序。
  • "Before setting MicroTask." 已记录。
  • setTimeout 对其任务进行排队
  • queueMicrotask 将其微任务排队
  • "After setting MicroTask." 已记录。
  • 任务调度到此结束,微任务处理开始
    • 第一个微任务正在处理来自 Promise.reject().then(success).catch(fooBarFailure1); 的拒绝:拒绝由 then 创建的承诺,排队一个微任务以在返回的承诺 then 上调用拒绝处理程序。
    • 第二个微任务正在处理来自 Promise.reject().then(success, fooBarFailure2); 的拒绝:拒绝承诺,调用 fooBarFailure2
      • "Failure 2" 已记录。
    • 第三个微任务来自 queueMicrotask,它正在运行。
      • "From MicroTask!" 已记录。
    • 上面安排的对 fooBarFailure1 的微任务调用运行。
      • "Failure 1" 已记录。
  • 下一个任务运行,调用定时器回调
    • "This Timeout was set before the MicroTask!" 已记录。