Як чекати, поки вирішиться JavaScript Promise, перш ніж відновити роботу?


107

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

Отже, я можу просто зателефонувати loadUrl(url).then(myFunc), і він зачекає, поки сторінка завантажиться, перш ніж виконати все, що myFuncє.

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

Недоліком цієї конструкції є те, що я постійно пишу анонімні функції з декількома рядками коду. Далі, поки я маю обхід (QUnit's 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>


якщо ви перекладете виклики додавання на функцію багаторазового використання, ви можете () за необхідності висушити. Ви також можете зробити багатоцільові обробники, керовані цим, щоб подавати тоді () виклики, наприклад .then(fnAppend.bind(myDiv)), які можуть значно скоротити анонів.
dandavis

З чим ви тестуєте? Якщо це сучасний браузер, або ви можете скористатися таким інструментом, як BabelJS, для транпіляції коду, це, безумовно, можливо.
Бенджамін Груенбаум,

Відповіді:


78

Мені цікаво, чи є якийсь спосіб отримати значення з Promise або зачекати (заблокувати / заснути), поки воно не вирішиться, подібно до IAsyncResult.WaitHandle.WaitOne () .NET. Я знаю, що 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

3
@dfoverdx - асинхронне кодування в Javascript завжди передбачає зворотні виклики, тому вам завжди потрібно визначати функцію (анонімну або іменовану). Наразі це ніяк не обійти.
jfriend00

2
Хоча ви можете використовувати позначення стрілок ES6, щоб зробити його більш стислим. (foo) => { ... }замістьfunction(foo) { ... }
Роб Х

1
Додаючи коментар до @RobH досить часто, ви також можете писати, foo => single.expression(here)щоб позбутися фігурних та круглих дужок.
begie

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.
Джош Хабдас

9
Це просто завершує обіцянку. Функція асинхронізації нічого не чекатиме під час її запуску, вона просто поверне обіцянку. async/ await- це ще один синтаксис для Promise.then.
rustyx

Ви абсолютно праві , asyncне чекатиме ... якщо вона не дає використання await. Приклад ES2016 / ES7 як і раніше неможливо запустити ТАК, ось ось код, який я написав три роки тому, демонструючи поведінку.
Джош Хабдас

7

Інший варіант - використовувати Promise.all для очікування вирішення масиву обіцянок. У наведеному нижче коді показано, як чекати, поки всі обіцянки будуть вирішені, а потім розглядати результати, як тільки вони будуть готові (оскільки це, здавалося, було метою питання); Однак, start, очевидно, може виводитись до того, як середній вирішиться, просто додавши цей код до того, як він викличе.

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>


1

Ви можете це зробити вручну. (Я знаю, що це не є чудовим рішенням, але ..) whileцикл використання, поки resultне має значення

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

це спожило б весь процесор під час очікування.
Лукаш Гумінський

це жахливе рішення
JSON
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.