Как дождаться разрешения обещания JavaScript перед возобновлением работы?


108

Я провожу модульное тестирование. Платформа тестирования загружает страницу в iFrame, а затем выполняет утверждения для этой страницы. Перед началом каждого теста я создаю объект, Promiseкоторый устанавливает onloadсобытие iFrame для вызова resolve(), устанавливает iFrame srcи возвращает обещание.

Итак, я могу просто позвонить loadUrl(url).then(myFunc), и он будет ждать загрузки страницы перед выполнением того, что myFuncесть.

Я использую этот вид шаблонов повсюду в своих тестах (не только для загрузки URL-адресов), в первую очередь для того, чтобы позволить изменениям в DOM (например, имитировать нажатие кнопки и ждать, пока div скрываются и отображаются).

Обратной стороной этого дизайна является то, что я постоянно пишу анонимные функции с несколькими строками кода в них. Кроме того, хотя у меня есть обходной путь (QUnit's assert.async()), тестовая функция, которая определяет обещания, завершается до того, как обещание будет выполнено.

Мне интересно, есть ли способ получить значение из a 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>


если вы поместите вызовы append в функцию многократного использования, вы можете при необходимости () выполнить DRY. вы также можете создать многоцелевые обработчики, управляемые этим для вызовов then (), например .then(fnAppend.bind(myDiv)), которые могут значительно сократить количество анонов.
— dandavis 07

С чем вы тестируете? Если это современный браузер или вы можете использовать такой инструмент, как BabelJS, для транспиляции кода, это, безусловно, возможно.
— Бенджамин Грюнбаум

Ответы:


80

Мне интересно, есть ли способ получить значение из обещания или подождать (блокировать / спать), пока оно не будет разрешено, аналогично .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, как 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
});

3
Правильно, я использовал then()то, что делал. Мне просто не нравится писать function() { ... }все время. Это загромождает код.
— dx_over_dt 08

3
@dfoverdx - асинхронное кодирование в Javascript всегда включает обратные вызовы, поэтому вам всегда нужно определять функцию (анонимную или именованную). В настоящее время нет никакого способа обойти это.
— jfriend00 08

2
Хотя вы можете использовать обозначение стрелок ES6, чтобы сделать его более лаконичным. (foo) => { ... }вместоfunction(foo) { ... }
— Rob H

1
Часто добавляя в комментарий @RobH, вы также можете написать, foo => single.expression(here)чтобы избавиться от фигурных и круглых скобок.
— Беги

3
@dfoverdx - Через три года после моего ответа я обновил его, добавив ES7 asyncи awaitсинтаксис в качестве опции, которая теперь доступна в node.js и в современных браузерах или через транспилеры.
— jfriend00

15

Если вы используете ES2016, вы можете использовать asyncи awaitи делать что-то вроде:

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

Если вы используете ES2015, вы можете использовать генераторы . Если вам не нравится синтаксис, вы можете абстрагироваться от него, используя asyncслужебную функцию, как описано здесь. .

Если вы используете ES5, вам, вероятно, понадобится библиотека, такая как Bluebird чтобы дать вам больше контроля.

Наконец, если ваша среда выполнения поддерживает ES2015, уже порядок выполнения может быть сохранен с помощью параллелизма с использованием Fetch Injection .


1
Ваш пример генератора не работает, ему не хватает бегуна. Пожалуйста, не рекомендуйте больше генераторы, если вы можете просто транспилировать ES8 async/ await.
— Берги

3
Спасибо за ваш отзыв. Я удалил пример генератора и вместо этого связался с более подробным руководством по генератору со связанной библиотекой OSS.
— Джош Хабдас 05

11
Это просто завершает обещание. Когда вы запустите асинхронную функцию, она ничего не будет ждать, она просто вернет обещание. async/ await- это просто еще один синтаксис для Promise.then.
— rustyx

Вы абсолютно правы , asyncне будет ждать ... если она не дает использования await. Пример ES2016 / ES7 по-прежнему не может быть запущен. Итак, вот код, который я написал три года назад, демонстрирующий поведение.
— Джош Хабдас

6

Другой вариант - использовать Promise.all для ожидания разрешения массива обещаний. В приведенном ниже коде показано, как дождаться выполнения всех обещаний, а затем обрабатывать результаты, когда все они будут готовы (поскольку это, по-видимому, и было целью вопроса); Тем не менее, очевидно, что start может выводиться до того, как middle будет разрешено, просто добавив этот код до того, как он вызовет resolve.

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>


Это тоже не сработает. Это только заставляет выполнять обещания по порядку. Все, что находится после kickOff (), будет выполняться до завершения обещаний.
— Филип Рего

1

Вы можете сделать это вручную. (Я знаю, что это не лучшее решение, но ...) используйте whileцикл, пока resultон не перестанет иметь значение

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});

1
это потребовало бы всего процессора во время ожидания.
— Лукаш Гумински,

это ужасное решение
— JSON

Да, но это единственный способ, который будет работать без вложенных функций или обещаний. Даже тогда строки после вложенных функций / обещаний выполняются до их завершения.
— Филип Рего

Это не сработает. Поскольку вы находитесь в бесконечном цикле, ожидая resultизменения, у результата никогда не будет возможности измениться, поскольку цикл событий никогда не сможет обработать что-либо еще. И resultэто просто аргумент функции, который в любом случае никогда не меняется.
— jfriend00
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.