为什么JavaScript ES6 Promise在解决(resolve)后继续执行?

158

据我理解,承诺是可以resolve()或reject()的东西,但令我惊讶的是,在调用resolve或reject后,承诺中的代码仍然会继续执行。

我认为resolve或reject是一种异步友好的退出或返回版本,它将停止所有立即执行的函数。

有人能解释一下为什么在调用resolve命令后以下示例有时会显示console.log:

var call = function() {
    return new Promise(function(resolve, reject) {
        resolve();
        console.log("Doing more stuff, should not be visible after a resolve!");
    });
};

call().then(function() {
    console.log("resolved");
});

jsbin

注:该内容为一个HTML标签,其中包含一个带有链接的段落,在新窗口中打开网页编辑器jsbin.com/sukacinayu/1/edit,并可查看控制台输出。

20
这是一个合理的问题,但不管怎样,JS只会像你告诉它要做的那样一个句子接一个句子地执行。resolve()不是JS的控制语句,也不会像魔法般具有return的效果,它只是一个函数调用,是的,在它之后执行会继续进行。 - user663031
这是一个好问题,即使在阅读了所有回答后,我仍不确定最佳实践... - Gabriel Glenn
1
我认为误解来自于你使用resolve()时终止的内容:在调用resolve()后,Promise确实已经被解决,但正如其他人已经说过的那样,这并不意味着终止Promise的函数也已经完成了其职责,因此它会一直执行,直到达到“正常”终止。 - Giuseppe Bertone
3个回答

215
JavaScript有一个概念叫做"执行到完成"。除非出现错误,函数会一直执行直到遇到return语句或者函数结束。函数外的其他代码不能干扰它的执行(除非出现错误)。
如果你想让resolve()退出初始化函数,你需要在前面加上return
return new Promise(function(resolve, reject) {
    return resolve();
    console.log("Not doing more stuff after a return statement");
});

5
@Alnitak resolve本身并不是异步的,它是完全同步的。尽管使用严格的ES6 API,无法观察到它是同步还是异步的。 - Esailija
4
@Esailija 好的,也许我表达不清楚。有些人认为调用 resolve 会立即调用任何已注册的回调函数,使其成为当前调用栈的一部分。但事实并非如此,它只是将回调函数排队(你是正确的,它不是异步的,但它只是执行它的任务并立即终止)。 - Alnitak
19
在您的编辑中,您说“return resolve();”,这似乎不太寻常。为了使我确信那里没有重要的事情发生,我不得不阅读文档并发现(1)resolve()似乎不返回任何重要内容,以及(2)初始化回调的返回值似乎没有被使用。难道说“resolve(); return;”不更清晰明了,从而避免这种干扰吗? - Don Hatch
1
@DonHatch 是的,这也可以。也许我太熟悉 Promise 了,看不出它是一个干扰因素 :-) 然而,if (err) return reject(err) 是一个常见的惯用语,其中返回值也不重要。 - Bergi
1
你提供的 return 建议让我免于脑出血。你因此赢得了一枚点赞。 - xploreraj
显示剩余2条评论

37
当您使用resolve解决一个Promise时,根据规范所需调用的回调函数仍然需要异步调用。这是为了确保在使用Promises进行同步和异步操作混合时具有一致的行为。
因此当您调用resolve时,回调函数会被排队,并且函数执行会立即继续执行resolve()调用后的任何代码。
只有当JS事件循环重新获得控制权时,回调函数才能从队列中删除并实际调用。

1
回调队列是在A+规范或ES6中记录的? - thefourtheye
6
@thefourtheye说,事件循环规范现在实际上是HTML5的一部分。ES6定义了一个内部方法叫做EnqueueJob,它被.then调用。 - Felix Kling
@thefourtheye:实际上,ES6 似乎也定义了队列:https://people.mozilla.org/~jorendorff/es6-draft.html#sec-jobs-and-job-queues。我猜这与事件循环有关。 - Felix Kling
@FelixKling 谢谢你提供的链接 - 我知道它是这样工作的,但无法引用具体章节。 - Alnitak
3
@FelixKling,这是有关微任务/宏任务的内容,以下是规范中关于“推迟执行”的部分:“当没有正在运行的执行上下文并且执行上下文堆栈为空时,ECMAScript实现将从作业队列中删除第一个PendingJob,并使用其中包含的信息创建执行上下文,并开始执行相关联的Job抽象操作。” - Benjamin Gruenbaum

0

resolve() 函数与 return 完全不同。它仅表示使用 then() 方法注册的回调函数的参数现在已准备就绪,回调函数可以潜在地离开作业队列(或微任务队列)并进入主 JS 调用堆栈,但只有当所有同步代码和先于此代码进入队列的异步代码运行完成时才会发生这种情况。console.log("Not doing more stuff after a return statement"); 这个语句在您的代码中是同步代码,并且它具有优先级高于异步代码。这就是为什么它首先运行的原因。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接