Jak czekać na rozwiązanie obietnicy JavaScript przed wznowieniem funkcji?

108

Robię testy jednostkowe. Środowisko testowe ładuje stronę do iFrame, a następnie uruchamia potwierdzenia na tej stronie. Przed rozpoczęciem każdego testu tworzę element, Promisektóry ustawia onloadzdarzenie iFrame do wywołania resolve(), ustawia iFrame srci zwraca obietnicę.

Mogę więc po prostu zadzwonić loadUrl(url).then(myFunc)i poczekać na załadowanie strony przed wykonaniem tego, co myFuncjest.

Używam tego rodzaju wzorca w moich testach (nie tylko do ładowania adresów URL), głównie po to, aby umożliwić wprowadzenie zmian w DOM (np. Naśladuj kliknięcie przycisku i czekaj, aż elementy div się ukryją i pokażą).

Wadą tego projektu jest to, że ciągle piszę anonimowe funkcje z kilkoma wierszami kodu. Ponadto, mimo że mam obejście (QUnit assert.async()), funkcja testowa, która definiuje obietnice, kończy się przed uruchomieniem obietnicy.

Zastanawiam się, czy istnieje sposób, aby uzyskać wartość z a Promiselub czekać (blok / uśpienie), aż zostanie rozwiązany, podobnie jak w .NET IAsyncResult.WaitHandle.WaitOne(). Wiem, że JavaScript jest jednowątkowy, ale mam nadzieję, że nie oznacza to, że funkcja nie może dać.

Zasadniczo, czy istnieje sposób na wyplucie wyników we właściwej kolejności?

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
źródło
jeśli umieścisz wywołania append w funkcji wielokrotnego użytku, możesz następnie () w razie potrzeby, aby DRY. możesz także utworzyć wielozadaniowe programy obsługi, sterowane przez to, aby podawać wywołania then (), na przykład .then(fnAppend.bind(myDiv)), które mogą znacznie skrócić anons.
dandavis
Z czym testujesz? Jeśli jest to nowoczesna przeglądarka lub możesz użyć narzędzia takiego jak BabelJS do transpozycji kodu, jest to z pewnością możliwe.
Benjamin Gruenbaum

Odpowiedzi:

81

Zastanawiam się, czy istnieje sposób, aby uzyskać wartość z Promise lub czekać (blok / uśpienie), aż zostanie rozwiązany, podobnie jak IAsyncResult.WaitHandle.WaitOne () .NET. Wiem, że JavaScript jest jednowątkowy, ale mam nadzieję, że nie oznacza to, że funkcja nie może dać.

Obecna generacja Javascript w przeglądarkach nie ma wait()lub, sleep()która pozwala na działanie innych rzeczy. Więc po prostu nie możesz zrobić tego, o co prosisz. Zamiast tego ma operacje asynchroniczne, które wykonają swoje zadanie, a następnie zadzwonią po zakończeniu (tak jak korzystałeś z obietnic).

Częściowo wynika to z pojedynczej wątkowości JavaScript. Jeśli pojedynczy wątek się kręci, żaden inny JavaScript nie może być wykonywany, dopóki ten wirujący wątek nie zostanie zakończony. ES6 wprowadza yieldi generatory, które pozwolą na kilka takich sztuczek kooperacyjnych, ale jesteśmy całkiem spora od możliwości użycia ich w szerokiej próbce zainstalowanych przeglądarek (mogą być używane w niektórych programach po stronie serwera, gdzie kontrolujesz silnik JS który jest używany).


Uważne zarządzanie kodem opartym na obietnicy może kontrolować kolejność wykonywania wielu operacji asynchronicznych.

Nie jestem pewien, czy dokładnie rozumiem, jaką kolejność próbujesz osiągnąć w swoim kodzie, ale możesz zrobić coś takiego, używając istniejącej kickOff()funkcji, a następnie dołączyć .then()do niej procedurę obsługi po wywołaniu jej:

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);
});

To zwróci dane wyjściowe w gwarantowanej kolejności - na przykład:

start
middle
end

Aktualizacja w 2018 r. (Trzy lata po napisaniu tej odpowiedzi):

Jeśli dokonasz transpozycji kodu lub uruchomisz go w środowisku obsługującym funkcje ES7, takie jak asynci await, możesz teraz użyć funkcji, awaitaby kod „pojawił się” i czekał na wynik obietnicy. Nadal rozwija się obiecująco. Nadal nie blokuje całego Javascript, ale umożliwia pisanie sekwencyjnych operacji w bardziej przyjaznej składni.

Zamiast robić rzeczy w ES6:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

Możesz to zrobić:

// 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
źródło
3
Racja, używanie then()jest tym, co robię. Po prostu nie lubię ciągle pisać function() { ... }. Zaśmieca kod.
dx_over_dt
3
@dfoverdx - kodowanie asynchroniczne w JavaScript zawsze obejmuje wywołania zwrotne, więc zawsze musisz zdefiniować funkcję (anonimową lub nazwaną). Obecnie nie da się tego obejść.
jfriend00
2
Chociaż możesz użyć notacji strzałkowej ES6, aby uczynić ją bardziej zwięzłą. (foo) => { ... }zamiastfunction(foo) { ... }
Rob H
1
Dodając do komentarza @RobH dość często możesz także napisać, foo => single.expression(here)aby pozbyć się nawiasów klamrowych i okrągłych.
Begie
3
@dfoverdx - Trzy lata po mojej odpowiedzi zaktualizowałem ją o ES7 asynci awaitskładnię jako opcję, która jest teraz dostępna w node.js oraz w nowoczesnych przeglądarkach lub przez transpilery.
jfriend00
16

Jeśli używasz ES2016, możesz użyć asynci awaiti zrobić coś takiego:

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

Jeśli używasz ES2015, możesz użyć Generatorów . Jeśli nie podoba ci się składnia, możesz ją wyodrębnić za pomocą asyncfunkcji narzędzia, jak wyjaśniono tutaj .

Jeśli używasz ES5, prawdopodobnie będziesz chciał, aby biblioteka taka jak Bluebird zapewniła Ci większą kontrolę.

Wreszcie, jeśli środowisko uruchomieniowe obsługuje ES2015, już kolejność wykonywania może zostać zachowana z równoległością przy użyciu funkcji Fetch Injection .

Josh Habdas
źródło
1
Twój przykład generatora nie działa, brakuje mu biegacza. Nie polecaj już generatorów, jeśli możesz po prostu transpile ES8 async/ await.
Bergi
3
Dziękuję za twój komentarz. Usunąłem przykład Generatora i utworzyłem łącze do bardziej wszechstronnego samouczka Generatora z powiązaną biblioteką OSS.
Josh Habdas
10
To po prostu wypełnia obietnicę. Funkcja async nie będzie czekać na nic po uruchomieniu, po prostu zwróci obietnicę. async/ awaitto tylko kolejna składnia dla Promise.then.
rustyx
Masz rację asyncnie będzie czekać ... chyba że daje użyciu await. Przykład ES2016 / ES7 nadal nie może zostać uruchomiony SO, więc oto kod, który napisałem trzy lata temu, demonstrujący to zachowanie.
Josh Habdas
7

Inną opcją jest użycie Promise.all, aby poczekać na rozwiązanie szeregu obietnic. Poniższy kod pokazuje, jak czekać na spełnienie wszystkich obietnic, a następnie radzić sobie z wynikami, gdy wszystkie są gotowe (ponieważ wydawało się, że to jest cel pytania); Jednak start może oczywiście wyprowadzić, zanim middle zostanie rozwiązany, po prostu dodając ten kod przed wywołaniem resell.

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>

Stan Kurdziel
źródło
1

Możesz to zrobić ręcznie. (Wiem, że to nie jest świetne rozwiązanie, ale…) używaj whilepętli, dopóki resultnie będzie miała wartości

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});
Guga Nemsitsveridze
źródło
zajęłoby to cały procesor podczas oczekiwania.
Łukasz Guminski
to jest okropne rozwiązanie
JSON