为什么JavaScript承诺然后处理程序运行在其他代码之后

Why does JavaScript Promise then handler run after other code?

本文关键字:其他 代码 之后 运行 程序 JavaScript 承诺 然后 后处理 为什么      更新时间:2023-09-26

我只是想提高我对JavaScript承诺如何工作的理解。我创建了以下情况:

LOG 'FOO'
RUN CALLBACK LOGGING 'CALLBACK'
LOG 'BAR'

期望所有的函数都能立即完成(我的意思是它们不会花费过多/未知的时间来完成,而你会使用异步操作来完成),这样上面的操作顺序就会按照这个顺序发生。

你可以这样写:

function foo(cb) {
  // LOG 'FOO'
  console.log('foo');
  // RUN CALLBACK
  cb();
}
function callback() {
  // LOG 'CALLBACK'
  console.log('callback');
}
foo(callback);
console.log('bar');

这将根据我在开始时指定的情况产生预期的输出。

> foo
> callback
> bar

也可以这样写:

function foo() {
  return new Promise((resolve) => {
    // LOG 'FOO'
    console.log('foo');
    return resolve(null);
  });
}
function callback() {
  // LOG 'CALLBACK'
  console.log('callback');
}
foo().then(callback);
// LOG 'BAR'
console.log('bar');

这种情况产生如下结果:

> foo
> bar
> callback

这是我不清楚的地方,因为我希望foo立即完成,以便callbackbar日志'bar'之前运行并记录'callback'

相关规格在这里:

  1. Promises/A+ point 2.2.4:

    在执行上下文堆栈只包含平台代码之前,不能调用

    onFulfilledonRejected。[3.1]。

    和注3.1(强调我的):

    这里的"平台代码"是指引擎、环境和承诺实现代码。在实践中,该要求确保onFulfilledonRejected在调用事件循环之后异步执行,并且具有新的堆栈。这可以通过"宏任务"机制(如setTimeoutsetImmediate)或"微任务"机制(如MutationObserverprocess.nextTick)来实现。由于承诺实现被认为是平台代码,因此它本身可能包含一个任务调度队列或"蹦床",其中调用处理程序。

  2. ECMAScript 6.0(基于Promises/A+)有点难以清晰地摘录,但then解析如第25.4.5.3.1节所示:

  3. 如果promise s [[PromiseState]] internal slot的值为"fulfilled"

    。设valuepromise的[[PromiseResult]]内部槽位的值。

    b。执行EnqueueJob("PromiseJobs", PromiseReactionJob, «‍fulfillReaction, value»)。

  4. 如果promise的[[PromiseState]]内部槽位的值为"rejected"

    。设reasonpromise的[[PromiseResult]]内部槽位的值。

    b。执行EnqueueJob("PromiseJobs", PromiseReactionJob, «‍rejectReaction, reason»)

重要的EnqueueJob操作在第8.4节("作业和作业队列")中定义,在前言(粗体是我的)中有如下特点:

只有当没有正在运行的执行上下文且执行上下文堆栈为空时,才可以启动Job的执行。[…Job一旦开始执行,就会一直执行到完成。在当前运行的作业完成之前,不能启动其他作业。

在实践中,这使您可以编写一些简单而一致的语句:

  • 你可以指望thencatch(等)总是异步行为,从不同步。你永远不会在同一个堆栈上看到多个thencatch处理程序,即使一个Promise在另一个Promise中显式解析。这也意味着递归的Promise执行不会像普通的函数调用那样有堆栈溢出的风险,尽管在病态情况下,如果不小心使用递归闭包,仍然会耗尽堆空间。
  • thencatch处理程序中排队的耗时操作永远不会阻塞当前线程,即使承诺已经解决,所以你可以将许多异步操作排队,而不必担心顺序或承诺状态。在thencatch之外永远不会有封闭的try块,即使在已经解决的Promise上调用then,所以平台是否应该处理抛出的异常没有歧义。

由于承诺的工作方式,这是不可能的。即使是立即解析的promise也会在下一个tick运行,你需要的是同步函数而不是promise。

请看下面的例子:

setTimeout(function() {
    console.log("test");
}, 0);

console.log("test2");

在不删除setTimeout函数的情况下,在test2之前打印test是不可能的,因为即使wait参数为0,它将在下一个tick中运行,这意味着它将在所有同步代码运行时运行。

我真的不想这么直白,但这是因为规范说它们是这样工作的。如果你需要一段代码在承诺内的代码完成后的某个时间点运行,那么你应该利用承诺链。一旦您将异步代码引入到混合代码中,尝试将其与依赖的、同步的代码混合在一起是一个坏主意。

菊花链承诺当你需要依赖异步代码时:

function foo() {
    console.log('foo');
    return Promise.resolve();
}
function callback() {
    console.log('callback');
}
function consoler() {
    console.log('bar');
}
foo().then(callback).then(consoler);

生产:

foo
callback
bar