如何在继续函数之前等待JavaScript Promise解决?

184
我正在进行一些单元测试。测试框架将一个页面加载到iFrame中,然后对该页面运行断言。在每个测试开始之前,我都会创建一个 Promise,该 Promise 会将 iFrame 的 onload 事件设置为调用 resolve(),设置 iFrame 的 src,并返回 Promise。
因此,我只需调用 loadUrl(url).then(myFunc),它就会在执行任何 myFunc 之前等待页面加载。
我在我的测试中(不仅限于加载 URL),经常使用这种模式,主要是为了允许 DOM 发生更改(例如模拟单击按钮,并等待 div 显示和隐藏)。
这种设计的缺点是,我不断地编写具有几行代码的匿名函数。此外,虽然我有一种解决方法(QUnit 的 assert.async()),但定义 Promises 的测试函数在 promise 运行之前就完成了。
我想知道是否有任何方法可以从 Promise 获取值或等待(阻塞/休眠)直到它已解析,类似于 .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>


如果将追加调用放入可重用函数中,则可以根据需要使用then()来DRY。您还可以制作多用途处理程序,由_this_引导以提供then()调用,例如.then(fnAppend.bind(myDiv)),这可以大大减少匿名函数。 - dandavis
你使用什么进行测试?如果是现代浏览器或者你可以使用像BabelJS这样的工具来转译你的代码,那么这肯定是可行的。 - Benjamin Gruenbaum
4个回答

119
当前的JavaScript浏览器不支持wait()或者sleep()来让其他任务运行。所以,你无法完成你所想做的那件事情。相反,它提供了异步操作,在完成时会调用你(就像你一直在使用的promises)。
这部分是由于JavaScript是单线程的。如果单个线程正在运转,那么直到该线程停止执行为止,没有其他JavaScript可以执行。ES6引入了yield和生成器,将允许一些协作技巧,但我们离能够在广泛的已安装浏览器中使用它们还有很长的路要走(它们可以在一些控制JS引擎的服务器端开发中使用)。
基于promise的代码的仔细管理可以控制许多异步操作的执行顺序。
我不确定您在代码中要实现的确切顺序,但您可以使用现有的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);
});

这将按照保证的顺序返回输出 - 就像这样:

start
middle
end

2018年更新(此回答撰写三年后):

如果你将代码转换或在支持ES7特性(如asyncawait)的环境中运行代码,你现在可以使用await来使你的代码“看起来”等待Promise的结果。这仍然是使用Promise编程,它并没有阻塞所有的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 函数在函数体内第一次遇到 await 就返回一个 Promise ,因此对于调用者来说,异步函数仍然是非阻塞的,调用者必须处理返回的 Promise 并从该 Promise 中获取结果。 但是,在 async 函数内部,您可以使用 await 在 Promise 上编写更类似于顺序执行的代码。请记住,只有在等待 Promise 时,await 才能发挥实际作用,因此为了使用 async/await,您所有的异步操作都必须基于 Promise。


3
好的,使用then()是我一直在做的事情。我只是不喜欢一直写function() { ... },它会使代码变得混乱。 - dx_over_dt
3
在 JavaScript 中,异步编程总是涉及到回调函数的使用,因此你总是需要定义一个函数(匿名或命名)。目前没有其他替代方法。 - jfriend00
4
三年后我更新了我的回答,加入了 ES7 的 asyncawait 语法作为选项,现在已经可以在 Node.js 和现代浏览器中使用,或者通过转译器实现。 - jfriend00
2
把锤子砸在你的额头上(我也犯过这样的错误),async/await很棒,但实际上只是语法糖(这篇文章写得非常好),它允许你在顶层编写会以一些形式出现在.then()中的代码块。事实上,当它被转译时……仍然没有办法在真正的主线程里等待,比如说,一个命令行的nodejs应用程序,让最后一行输出在顶级位置。 - Frank N
4
这不行,因为someValue未定义。试图等待结果太复杂了,简直是疯狂。 - Philip Rego
显示剩余11条评论

21
如果使用ES2016,您可以使用“async”和“await”,并进行以下操作:
(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

如果您使用的是ES2015,您可以使用生成器。如果您不喜欢这种语法,可以使用一个async实用函数来抽象它,如此处所述
如果您使用的是ES5,您可能需要像Bluebird这样的库来给您更多的控制。
最后,如果您的运行时已经支持ES2015,则可以使用Fetch Injection以并行方式保留执行顺序。

1
你的生成器示例无法工作,缺少一个运行程序。请不要再推荐生成器了,因为你可以直接转译ES8的async/await - Bergi
3
感谢您的反馈。我已经移除了生成器示例,并链接到了一个更全面的生成器教程,其中包含相关的开源库。 - vhs
41
这只是将 Promise 包装起来。当您运行它时,async 函数将不会等待任何东西,它只会返回一个 Promise。 async/await 只是 Promise.then 的另一种语法形式。 - rustyx
@rustyx 太好了,它返回了另一个承诺。但是当我在then块中将我的代码链接到这个承诺时,你如何解释它是异步的呢?当我在这个async/await包装器之后简单地启动代码时,它看起来是同步执行的。 - undefined

12

另一种选择是使用Promise.all来等待一组promise完成并在完成后对它们进行操作。

下面的代码展示了如何等待所有promise完成,然后在它们都准备好时处理结果(因为这似乎是问题的目标);同时为了说明目的,在执行过程中显示输出(end在middle之前结束)。

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>


2
这也行不通。这只是让承诺按顺序运行。在kickOff()之后的任何内容都将在承诺完成之前运行。 - Philip Rego
@PhilipRego - 正确,任何你想在之后运行的东西都需要在 Promises.all 的块中。我已经更新了代码,在执行期间追加输出(结束打印在中间之前 - 承诺可能会无序运行),但是 then 块等待它们全部完成,然后按顺序处理结果。 - Stan Kurdziel
顺便说一句:您可以 fork 这个 Fiddle 并玩弄其中的代码:https://jsfiddle.net/akasek/4uxnkrez/12/ - Stan Kurdziel

0
我做了一个小的包装器/内核来处理异步/非异步回调依赖关系,以事件方式进行(对于我的自我理解来说非常有趣)。
在内部,内核执行了一些小而不易理解的Promise链逻辑。
它创建了一个事件对象,可以用来在(掩码的)Promise链中传递应用上下文。
function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// your start
qk.addEventListener('my_kickoff_event', async (e) => {
    $("#output").append("start");
    await timeout(1000);
}, 'start');

// your middle
qk.addEventListener('my_kickoff_event', (e) => {
    $("#output").append(" middle");
}, 'middle', 'start'); // <<< dependency: middle needs start

// kickoff and end
qk.dispatchEvent(new QKE('my_kickoff_event')).then((e) => {
   // it's still only Promise in the end
   $("#output").append(" end");
});
// or await qk.dispatchEvent(new QKE('my_kickoff_event'));

它的设计并不是为了在性能方面做到最佳解决方案,而是为了在具有许多无法轻松共享 Promise 变量的典型应用程序中易于使用。
更多这里 尝试实时示例

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