Как дождаться выполнения обещания JavaScript перед возобновлением функции?
Я делаю некоторые модульные тесты. Тестовая среда загружает страницу в iFrame, а затем запускает утверждения против этой страницы. Перед началом каждого теста я создаю Promise
, который устанавливает событие iFrame onload
для вызова resolve()
, устанавливает iFrame src
и возвращает обещание.
Итак, я могу просто позвонить loadUrl(url).then(myFunc)
, и он будет ожидать загрузки страницы, прежде чем выполнять myFunc
.
Я использую этот тип шаблонов во всех своих тестах (не только для загрузки URL-адресов), в первую очередь для того, чтобы позволить изменениям в DOM (например, имитировать нажатие кнопки и ждать, пока divs будут скрыты и показываться).
Недостатком этого дизайна является то, что я постоянно пишу анонимные функции с несколькими строками кода в них. Кроме того, пока у меня есть работа (QUnit assert.async()
), тестовая функция, которая определяет promises, завершается до того, как обетование будет запущено.
Мне интересно, есть ли способ получить значение из 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>
Ответы
Ответ 1
Мне интересно, есть ли способ получить ценность от обещания или ждать (блокировать/спящий режим) до тех пор, пока он не будет разрешен, подобно IAsyncResult.WaitHandle.WaitOne().NET. Я знаю, что JavaScript однопоточен, но я надеюсь, это не значит, что функция не может уступить.
Нынешнее поколение Javascript в браузерах не имеет wait()
или sleep()
что позволяет запускать другие вещи. Таким образом, вы просто не можете делать то, что вы просите. Вместо этого у него есть асинхронные операции, которые будут выполнять свою работу, а затем вызывать вас, когда они будут сделаны (как вы использовали обещания).
Часть этого из-за Javascript одинарной резьбы. Если один поток вращается, тогда никакой другой Javascript не может выполняться до тех пор, пока этот вращающийся поток не будет выполнен. ES6 представляет yield
и генераторы, которые позволят использовать некоторые подобные трюки, но мы вполне можем использовать их в широком образце установленных браузеров (их можно использовать в некоторых серверных разработках, где вы управляете JS двигатель, который используется).
Тщательное управление кодом на основе обещаний может контролировать порядок выполнения для многих асинхронных операций.
Я не уверен, что я точно понимаю, какой порядок вы пытаетесь достичь в своем коде, но вы можете сделать что-то подобное с помощью существующей функции kickOff()
, а затем 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 году (через три года после написания этого ответа):
Если вы либо transpile кода или запустить свой код в среде, которая поддерживает функцию ES7, такую как async
и await
, теперь вы можете использовать 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
});
Ответ 2
Если вы используете ES2016, вы можете использовать async
и await
и сделать что-то вроде:
(async () => {
const data = await fetch(url)
myFunc(data)
}())
Если вы используете ES2015, вы можете использовать Генераторы. Если вам не нравится синтаксис, вы можете абстрагировать его с помощью служебной функции async
, как объяснено здесь.
Если вы используете ES5, вам, вероятно, понадобится библиотека типа Bluebird, которая даст вам больше контроля.
Наконец, если ваша среда выполнения поддерживает ES2015, порядок выполнения может быть сохранен с параллелизмом с помощью Fetch Injection.
Ответ 3
Другой вариант - использовать Promise.all, чтобы дождаться разрешения массива обещаний.
В приведенном ниже коде показано, как дождаться выполнения всех обещаний, а затем обработать результаты, когда все они будут готовы (поскольку это казалось целью вопроса); Тем не менее, запуск, очевидно, может быть выведен до разрешения середины, просто добавив этот код, прежде чем он вызовет разрешение.
function kickOff() {
let start = new Promise((resolve, reject) => resolve("start"))
let middle = new Promise((resolve, reject) => {
setTimeout(() => resolve(" middle"), 1000)
})
let end = new Promise((resolve, reject) => resolve(" end"))
Promise.all([start, middle, end]).then(results => {
results.forEach(result => $("#output").append(result))
})
}
kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>
Ответ 4
Вы можете сделать это вручную. (Я знаю, что это не очень хорошее решение, но..)
используйте цикл while
, пока result
не будет иметь значение
kickOff().then(function(result) {
while(true){
if (result === undefined) continue;
else {
$("#output").append(result);
return;
}
}
});