【问题标题】:Why are javascript promises asynchronous when calling only synchronous functions?为什么仅调用同步函数时javascript promises是异步的?
【发布时间】:2016-08-12 03:12:27
【问题描述】:

在测试中,我发现 JavaScript Promises总是是异步的,无论它们的链中是否包含任何异步函数。

这是一些显示控制台中操作顺序的代码。如果您运行它,您会看到即使每个函数都是同步的,输出也会显示两个 aPromise() 调用并行运行,而 "surprisingly this happens after run 2 finishes" 不会在运行 2 完成之前发生。

function aPromise() {
  return new Promise(function(resolve, reject) {
    console.log("making promise A")
    resolve(bPromise());
    console.log("promise A resolved")
  });
}


function bPromise() {
  return new Promise(function(resolve, reject) {
    console.log("making and resolving promise B")
    resolve();
  });
}

aPromise().then(function() {
  console.log("finish run 1");
}).then(function() {
  console.log("surprisingly this happens after run 2 finishes");
});
aPromise().then(function() {
  console.log("finish run 2");
})

输出到控制台:

making promise A
making and resolving promise B
promise A resolved
making promise A
making and resolving promise B
promise A resolved
finish run 1
finish run 2
surprisingly this happens after run 2 finishes

那么,为什么 JavaScript 的 promises 在只调用同步函数时是异步的?导致这种行为的幕后发生了什么?


附:为了更好地理解这一点,我实现了我自己的 Promise 系统,我发现让同步函数按照预期的顺序发生很容易,但是让它们并行发生是我只能通过在每个时间设置几毫秒的 setTimeout() 来完成的。解决(我的猜测是,这不是香草承诺发生的事情,它们实际上是多线程的)。

这对我的一个程序来说是一个小问题,我正在遍历树,将函数数组应用于每个节点,如果该节点已经运行了异步函数,则将函数放入队列中。大多数函数都是同步的,因此很少使用队列,但是在从回调(地狱)切换到 Promises 时,我遇到了一个问题,即由于 Promises 从不同步运行,队列几乎总是被使用。这不是一个大问题,但它有点像调试噩梦。

1 年后编辑

我最终编写了一些代码来处理这个问题。它不是非常彻底,但我已经成功地使用它来解决我遇到的问题。

var SyncPromise = function(fn) {
    var syncable = this;
    syncable.state = "pending";
    syncable.value;

    var wrappedFn = function(resolve, reject) {
        var fakeResolve = function(val) {
            syncable.value = val;
            syncable.state = "fulfilled";
            resolve(val);
        }

        fn(fakeResolve, reject);
    }

    var out = new Promise(wrappedFn);
    out.syncable = syncable;
    return out;
}

SyncPromise.resolved = function(result) {
    return new SyncPromise(function(resolve) { resolve(result); });
}

SyncPromise.all = function(promises) {
    for(var i = 0; i < promises.length; i++) {
        if(promises[i].syncable && promises[i].syncable.state == "fulfilled") {
            promises.splice(i, 1);
            i--;
        }
        // else console.log("syncable not fulfilled" + promises[i].syncable.state)
    }

    if(promises.length == 0)
        return SyncPromise.resolved();

    else
        return new SyncPromise(function(resolve) { Promise.all(promises).then(resolve); });
}

Promise.prototype.syncThen = function (nextFn) {
    if(this.syncable && this.syncable.state == "fulfilled") {
            //
        if(nextFn instanceof Promise) {
            return nextFn;
        }
        else if(typeof nextFn == "function") {
            var val = this.syncable.value;
            var out = nextFn(val);
            return new SyncPromise(function(resolve) { resolve(out); });
        }
        else {
            PINE.err("nextFn is not a function or promise", nextFn);
        }
    }

    else {
        // console.log("default promise");
        return this.then(nextFn);
    }
}

【问题讨论】:

  • 其他人可能会挖掘出具体的讨论,但总体理念是一致性/可预测性是界面设计的一个优点。一些早期的 Promise 实现并非如此,但随着人们被意外的错误所困扰,这种情绪发生了变化。如果你知道 Promise 总是被异步处理,那么推理 Promise 会容易很多。
  • 你知道什么样的bug吗?
  • 快速查看the spec,措辞是“将立即enqueue要调用的作业”,enqueued 事物可以调用的最快时间在当前函数结束后立即出现。 new Promise(r =&gt; r()).then(() =&gt; console.log('yay')); let i = 10; while (--i) console.log(i); 在 yay 之前记录 9..1
  • @SephReed:getPromise().then(function(x) { foo.bar(x); }); var foo = new Foo(); 的那种错误。它适用于 A+ 承诺,但不适用于 then 意外同步调用其回调。

标签: javascript asynchronous promise synchronous


【解决方案1】:

传递给 Promise 构造函数的回调总是同步调用,但传递给 then 的回调总是异步调用(你可以在用户空间实现中使用 setTimeout 延迟 0 来实现)。

将您的示例简化(并给出匿名函数的名称,以便我可以参考它们):

Promise.resolve().then(function callbackA () {
  console.log("finish run 1");
}).then(function callbackB () {
  console.log("surprisingly this happens after run 2 finishes");
});

Promise.resolve().then(function callbackC () {
  console.log("finish run 2");
})

仍然以相同的顺序给出输出:

finish run 1
finish run 2
surprisingly this happens after run 2 finishes

事件按以下顺序发生:

  1. 第一个承诺已解决(同步)
  2. callbackA 被添加到事件循环的队列中
  3. 第二个承诺已解决
  4. callbackC 被添加到事件循环的队列中
  5. 没有什么可做的,因此事件循环被访问,callbackA 在队列中排在第一位,因此它被执行,它不返回承诺,因此回调B 的中间承诺立即同步解决,这将回调B 附加到事件循环的队列。
  6. 没有什么可做的,所以事件循环被访问了,callbackC 在队列中是第一个所以它被执行。
  7. 没有什么可做的,所以事件循环被访问了,callbackB 在队列中首先被执行。

我能想到的解决您的问题的最简单方法是使用具有 Promise.prototype.isFulfilled 函数的库,您可以使用该函数来决定是否同步调用您的第二个回调。例如:

var Promise = require( 'bluebird' );                                                                                                                          

Promise.prototype._SEPH_syncThen = function ( callback ) { 
    return (
      this.isPending()
        ? this.then( callback )
        : Promise.resolve( callback( this.value() ) ) 
    );  
}

Promise.resolve()._SEPH_syncThen(function callbackA () {
  console.log("finish run 1");
})._SEPH_syncThen(function callbackB () {
  console.log("surprisingly this happens after run 2 finishes");
});

Promise.resolve()._SEPH_syncThen(function callbackC () {
  console.log("finish run 2");
})

这个输出:

finish run 1
surprisingly this happens after run 2 finishes
finish run 2

【讨论】:

  • 一个事件队列使得 Promise 总是异步执行。这是有道理的。你知道为什么做出这样的设计选择吗?这对我来说有点反直觉。
  • @SephReed 简短回答:一致性。长答案:质疑语言开发人员的设计选择超出了 Stack Overflow 的范围。
  • @SephReed 我相信有一个标准接口/API,无论其执行的代码如何,它都将始终具有标准行为。根据应用改变其功能可能会导致意外行为、复杂性和混乱。
  • @Pualpro 非常感谢您的工作。我真的希望找到一种方法来停止使用我自制的承诺系统(尽管我喜欢它),这似乎是一个完美的解决方案。我真的很感激。
  • 这种解决方法太可怕了。它不会捕获错误,它颠覆了 Promise 的设计,如果你以异步解决的 Promise 开始你的链,它甚至都不起作用。 OP 问题的正确解决方案是等待运行 2 以完成运行 1,方法是使用 then 显式链接它。
【解决方案2】:

您的代码很好,是您希望您的 Promise 独立运行并让它们以自己的方式执行,无论每个先完成。一旦您的代码是异步的,您就无法预测哪个将首先完成(由于 event loop 的异步性质)。

但是,如果您想在所有 promise 完成后捕获它们,您应该使用 Promise.all(相当于 $.when 是 jQuery)。 见:

【讨论】:

    猜你喜欢
    • 2012-02-25
    • 1970-01-01
    • 1970-01-01
    • 2021-08-07
    • 2019-02-15
    • 1970-01-01
    • 2018-10-07
    • 2012-07-25
    相关资源
    最近更新 更多