如何等待一个 JavaScript 承诺解决之前恢复功能?

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

因此,我可以调用 loadUrl(url).then(myFunc),它将等待页面加载后再执行 myFunc

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

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

我想知道是否有任何方法可以从 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>

392696 次浏览

我想知道是否有办法从一个承诺或 等待(阻塞/睡眠) ,直到它已经解决,类似于.NET 的 IAsyncResult WaitHandle WaitOne ()我知道 JavaScript 是 但我希望这并不意味着 不能屈服。

当前浏览器中的 Javascript 没有一个 wait()或者 sleep()允许其他东西运行。所以,你根本不能做你要求的事。相反,它具有异步操作,这些异步操作将执行它们的任务,然后在完成任务后调用您(正如您一直使用的承诺)。

部分原因是由于 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);
});

这将按照保证的顺序返回输出,如下所示:

start
middle
end

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

如果在支持 ES7特性(如 asyncawait)的环境中运行代码或调换代码,现在可以使用 await使代码“显示”以等待承诺的结果。它仍然在用承诺编程。它仍然不能阻止所有的 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
});

当第一个 await在函数体内被命中时,async函数返回一个承诺,因此对于调用者来说,一个异步函数仍然是非阻塞的,调用者仍然必须处理返回的承诺并从该承诺中获得结果。但是,在 async函数内部,您可以在承诺上使用 await编写更多类似于顺序的代码。请记住,如果您等待一个承诺,那么 await只能做一些有用的事情,因此为了使用 async/await,您的异步操作必须都是基于承诺的。

如果使用 ES2016,你可以使用 asyncawait,并做一些像:

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

如果使用 ES2015,可以使用 发电机。如果您不喜欢这种语法,可以使用 async实用功能作为 在这里解释将其抽象出来。

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

最后,如果您的运行时支持 ES2015,那么可以使用 取注射剂并行地保留已经执行的顺序。

另一种选择是使用 Promise.all 等待一系列承诺解决,然后采取行动。

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

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>