Как дождаться разрешения обещания 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>

dx_over_dt
источник
если вы поместите вызовы 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
});
jfriend00
источник
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