承诺输出在NodeJs和浏览器之间存在差异

3
以下代码块在Node和浏览器中执行的结果不同。当然,不同的环境,不同的处理时间,以及存在竞态条件。但根据我对Promises的理解,这段代码应该在不同环境下保持一致。 我期望的是Chrome / 浏览器的结果。我不希望看到NodeJs的结果。我不明白为什么每个newPromise的then链在masterPromise的then链继续之前没有完成。换句话说,因为一个新的Promise在then fn中返回到masterPromise promise-chain中,我希望新的Promise的then-chain在masterPromise promise-chain恢复之前完成。 如果有人能指出以下实现中的问题,并解释为什么NodeJs的结果是有效的,那就太好了! 使用chrome 44 and node 12.6。
'use strict';
var masterPromise = Promise.resolve();
var numbers = [ 1, 2, 3 ];

// function returns a new promise that fulfills in 100ms
// it logs two bits of information--one pre-resolve, & one post-resolve.
// because a `.then` is registered immediately, before the promised is
// fulfilled, i would expect the post-resolve console.log to be logged before
// any other logging
var returnNewPromise = function(number) {
    var resolve;
    var newPromise = new Promise(function(r) { resolve = r; });
    newPromise.then(function() { console.log('registered ' + number + ' (verbatim, syncronous echo)'); });
    setTimeout(function() {
        console.log('registered ' + number);
        resolve();
    }, 100);
    return newPromise;
};

numbers.forEach(function(number) {
    var getChildPromise = function(number) {
        return returnNewPromise(number);
    };
    return masterPromise.then(function() {
        return getChildPromise(number);
    });
});

节点:

registered 1
registered 2
registered 3
registered 1 (verbatim, syncronous echo)
registered 2 (verbatim, syncronous echo)
registered 3 (verbatim, syncronous echo)

Chrome:

registered 1
registered 1 (verbatim, syncronous echo)
registered 2
registered 2 (verbatim, syncronous echo)
registered 3
registered 3 (verbatim, syncronous echo)

你需要进一步解释为什么“基于你对 Promises 的理解”,应该期望其中一个行为。我认为这是不确定的,并且在相关规范下,这两种模式都是有效的行为。 - Pointy
@Pointy,好的,没问题!已更新。 - cdaringe
@Pointy,我再次更新以获得更窄的焦点。你看到了在语法上引起不确定性的原因吗?我的一个同事看了这个问题,认为它也应该是确定性的。然而,他遇到了和我一样的问题,尽管他制作了一个很棒的小提琴来演示这个问题!https://jsfiddle.net/kgwouxg7/1/ - cdaringe
这不是语法问题,而是语义问题。我完全看不出它应该是确定性的理由。不同的事件组之间没有相互依赖,因此调用顺序绝对没有任何影响。 - Pointy
2个回答

2

我不理解为什么每个新的Promise的then链在其resolve后不立即执行。

then回调函数不会立即调用。

var resolve;
new Promise(function (r) { resolve = r; })
    .then(function () { console.log(2); });
resolve();
console.log(1);

日志:

1
2

Promise的解决行为类似于没有延迟的setTimeout

更新:

setTimeout的比较并不完全正确,因为存在限制: https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout#Minimummaximum_delay_and_timeout_nesting 相似之处在于Promise是异步的。

更新2:

-----------+------------
wait       |  resolved
-----------+------------
timeout1   |             initial state
timeout2   |
-----------+------------
           | timeout1    after 100 ms
timeout2   |
-----------+------------
           | resolve1    then Chrome variant
           | timeout2
-----------+------------
           | timeout2    or Node variant
           | resolve1
-----------+------------

这两个变量都符合规范。看起来Chrome会在后续的超时回调之前排队解决回调。


是的,这就是我认为运行时可以产生任何结果并且是正确的原因。尽管人们可能想象.then()应该在承诺已经解决的情况下直接进行函数调用,但事实并非如此:回调应该在干净的堆栈上运行,因此它是通过一些基本上像这个答案所描述的机制调用的。因此,浏览器可能会在等待超时之前处理它,也可能不会。(这是我的意见,但这个答案仍然是正确的。) - Pointy
是的,这对我来说是个新闻。谢谢。很高兴知道它是一个nextTick-可能的操作。但它仍然没有解释以下内容,这与预期相悖。'newPromise' 在 'masterPromise' 的 then 链中的一个 then 中被返回。难道不应该在继续执行 masterPromise 的 then 链之前执行 newPromise 的每个 then 吗?我觉得这个约定没有被遵守。 - cdaringe
@cdaringe 不会的 - 为什么会发生这种情况呢? - Pointy
嘿@Pointy,我相信是因为这个规则。对吧?如果X有一个可解决对象,而该对象返回一个承诺Y,则承诺链会适应Y的状态,并应该解决出Y的then链。假设Y的thenable返回一个承诺Z。在这种情况下,Z应该实现,然后是任何Y thens,最终是X上剩余的任何内容! - cdaringe
@cdaringe 嗯,那可能很重要,但我不确定 "then" 回调函数的调用如何与其他异步环境相关联。 - Pointy

2
我不明白为什么每个newPromise的then链在masterPromise的then链继续之前没有完成。在then fn中,将一个新的Promise返回到masterPromise promise-chain中,所以它不应该等待那个之后才继续吗?
不是这样的。你误解了“masterPromise chain”的含义:实际上并不存在这样的东西。你有一个masterPromise,然后从它那里链接了三个不同的then调用。当masterPromise解决(立即),它会看到3个回调,并按照它们注册的顺序调用它们。它不关心这些回调做什么,无论它们是否异步,也不等待它们的promise结果。在你的情况下,它们都创建了promises,并通过它们推进了它们的子链,但这3个子链是完全独立的。
也许用更具描述性的日志记录来详细说明你正在做什么会有助于理解:
function delay(number) {
    console.log("creating promise for", number);
    return new Promise(function(resolve) {
        setTimeout(function() {
            console.log('resolving promise with', number);
            resolve(number);
        }, 100);
    });
};
function result(n) {
    console.log("received", number);
}

var masterPromise = Promise.resolve();

masterPromise.then(function(){ delay(1).then(result); }); // chain 1
masterPromise.then(function(){ delay(2).then(result); }); // chain 2
masterPromise.then(function(){ delay(3).then(result); }); // chain 3
console.log("created chains");

您在这里看到的日志是:
// .then chain 1    -------,
// .then chain 2    ------- \ -,
// .then chain 3    -------- \ -\ -,
created chains                |  |  |
                              |  |  | 3 then callbacks in the order they were registered
creating promise for 1      <´   |  |
// setTimeout 100   -----,       |  |
                          \     /   |
creating promise for 2     | <-´    |
// setTimeout 100   ------ |-,      /
                           |  \    /
creating promise for 3     |  | <-´
// setTimeout 100   ------ |- |-,
                           |  |  \
…                          |  |  | 3 timeout callbacks in the order they were scheduled
                           |  |  |
resolving promise with 1 <´   |  |
// resolve()                  |  |
[…]                           /  |
resolving promise with 2  <-´    |
// resolve()                     /
[…]                             /
resolving promise with 3   <---´
// resolve()
[…]

这里我们可以看到发生了3个(独立的)resolve()调用。它们会尽快安排它们各自的回调函数(result)。这里介绍了node和Chrome之间的区别:前者在同一个tick中执行相同时间(它们使用相同的超时)的timeout回调,而Chrome则使用单独的ticks。因此,在node中,“asap”是在三个回调之后,而在Chrome中则在它们之间。两种实现都很好。

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