如何在恢复功能之前等待 JavaScript Promise 解决?

How to wait for a JavaScript Promise to resolve before resuming function?

我正在做一些单元测试。测试框架将页面加载到 iFrame 中,然后 运行s 断言针对该页面。在每次测试开始之前,我创建了一个 Promise,它将 iFrame 的 onload 事件设置为调用 resolve(),设置 iFrame 的 src,以及 returns 承诺。

所以,我可以调用 loadUrl(url).then(myFunc),它会在执行任何 myFunc 之前等待页面加载。

我在我的测试中到处使用这种模式(不仅仅是为了加载 URL),主要是为了允许对 DOM 进行更改(例如,模仿点击按钮,然后等待用于隐藏和显示 div)。

这种设计的缺点是我经常编写匿名函数,其中只有几行代码。此外,虽然我有一个解决方法(QUnit 的 assert.async()),但定义承诺的测试函数在承诺 运行.

之前完成

我想知道是否有任何方法可以从 Promise 获取值或等待 (block/sleep) 直到它解析,类似于 .NET 的 IAsyncResult.WaitHandle.WaitOne()。我知道 JavaScript 是单线程的,但我希望这并不意味着函数不能产生。

本质上,有没有办法让下面的吐出结果顺序正确?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};

function getResultFrom(promise) {
  // todo
  return " end";
}

var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

I'm wondering if there is any way to get a value from a Promise or wait (block/sleep) until it has resolved, similar to .NET's IAsyncResult.WaitHandle.WaitOne(). I know JavaScript is single-threaded, but I'm hoping that doesn't mean that a function can't yield.

当前一代的 Javascript 浏览器中没有允许其他内容 运行 的 wait()sleep()。所以,你根本不能做你要求的。相反,它具有异步操作,这些操作会完成它们的工作,然后在它们完成后给您打电话(就像您一直在使用 promises 一样)。

部分原因是 Javascript 的单线程。如果单个线程正在旋转,则在该旋转线程完成之前,其他 Javascript 无法执行。 ES6 引入了 yield 和生成器,它们将允许一些类似的合作技巧,但我们要在广泛安装的浏览器中使用它们还有很长的路要走(它们可以用于某些服务器端开发,其中您控制正在使用的 JS 引擎)。


仔细管理基于承诺的代码可以控制许多异步操作的执行顺序。

我不确定我是否完全理解您要在代码中实现的顺序,但是您可以使用现有的 kickOff() 函数执行类似的操作,然后附加一个 .then() 调用它之后的处理程序:

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}

kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

这将 return 以有保证的顺序输出 - 像这样:

start
middle
end

2018年更新(写这个答案三年后):

如果您在支持 ES7 功能(例如 asyncawait)的环境中转译您的代码或 运行 您的代码,您现在可以使用 await使您的代码“出现”以等待承诺的结果。它仍然在用 promises 编程。它仍然不会阻止所有 Javascript,但它确实允许您以更友好的语法编写顺序操作。

而不是 ES6 的处理方式:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

你可以这样做:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}

wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});

async 函数 return 一旦第一个 await 在其函数体内被击中就承诺,因此对于调用者来说,异步函数仍然是非阻塞的,调用者必须仍然处理 returned 承诺并从该承诺中获得结果。但是,在 async 函数内部,您可以使用 await on promises 编写更多类似顺序的代码。请记住,await 仅在您等待承诺时才做一些有用的事情,因此为了使用 async/await,您的异步操作必须全部基于承诺。

如果使用 ES2016,您可以使用 asyncawait 并执行以下操作:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

如果使用 ES2015 你可以使用 Generators. If you don't like the syntax you can abstract it away using an async utility function as explained here.

如果使用 ES5,您可能需要像 Bluebird 这样的库来为您提供更多控制权。

最后,如果您的运行时支持 ES2015,则可以使用 Fetch Injection.

并行保留执行顺序

另一种选择是使用 Promise.all 等待一系列 promises 得到解决,然后对其采取行动。

下面的代码展示了如何等待所有的承诺解决,然后在它们都准备好后处理结果(因为这似乎是问题的 objective);同样出于说明目的,它显示了执行期间的输出(结束在中间之前完成)。

function append_output(suffix, value) {
  $("#output_"+suffix).append(value)
}

function kickOff() {
  let start = new Promise((resolve, reject) => {
    append_output("now", "start")
    resolve("start")
  })
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => {
      append_output("now", " middle")
      resolve(" middle")
    }, 1000)
  })
  let end = new Promise((resolve, reject) => {
    append_output("now", " end")
    resolve(" end")
  })

  Promise.all([start, middle, end]).then(results => {
    results.forEach(
      result => append_output("later", result))
  })
}

kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
Updated during execution: <div id="output_now"></div>
Updated after all have completed: <div id="output_later"></div>