Obietnice kodu JavaScript: wprowadzenie

Obietnice upraszczają obliczenia odroczone i asynchroniczne. Obietnica reprezentuje operację, która nie została jeszcze zakończona.

Jake Archibald
Jake Archibald

Przygotuj się na przełomowy moment w historii tworzenie stron internetowych.

[Zaczyna się perkusja]

W języku JavaScript pojawiły się obietnice!

[Wybuchają fajerwerki, z góry pada błyszczący papier, tłum szaleje]

W tym momencie należysz do jednej z tych kategorii:

  • Ludzie wiwatują wokół Ciebie, ale nie masz pewności, o co cię chodzi na temat witryny. Może nie do końca wiesz, co to jest „obietnica” Wzruszasz ramionami, ale ciężar błyszczącego papieru ciężarów u Ciebie. Jeśli tak, nie martwię się tym, zajęło mi wiele lat, aby zrozumieć, dlaczego to powinno mnie zainteresować. rzeczy. Prawdopodobnie chcesz zacząć od początku.
  • Świetnie Ci idzie! To w odpowiednim momencie, prawda? Ta funkcja Promise była już przez Ciebie używana ale irytuje Cię, że wszystkie implementacje mają nieco inny interfejs API. Czym jest interfejs API dla oficjalnej wersji JavaScriptu? Zacznij prawdopodobnie i skorzystaj z terminologii.
  • Wiedziałeś już o tym i szydzisz z tych, którzy podskakują, wiadomości. Daj się pochłonąć swojej wyższości, i przejdź od razu do dokumentacji interfejsu API.

Obsługa przeglądarek i kod polyfill

Obsługa przeglądarek

  • Chrome: 32.
  • Krawędź: 12.
  • Firefox: 29.
  • Safari: 8.

Źródło

Dostosowanie do specyfikacji przeglądarek, których nie przewidujemy w żaden sposób lub dodaj obietnice do innych przeglądarek i Node.js, zapoznaj się z informacjami kod polyfill (2K w formacie gzip).

O co chodzi?

JavaScript jest jednowątkowy, co oznacza, że dwa bity skryptu nie mogą być uruchamiane w tym samym czasie. muszą przechodzić jeden po drugim. W przeglądarkach JavaScript udostępnia wątek z mnóstwem innych rzeczy, które różnią się w zależności od przeglądarki przeglądarki. JavaScript zwykle znajduje się jednak w tej samej kolejce co malowanie, stylu, a także obsługi działań użytkownika (np. wyróżniania tekstu i interakcji za pomocą elementów sterujących formularza). Aktywność w jednym z tych elementów opóźnia pozostałe.

Jako człowiek masz wielowątkowość. Możesz pisać kilkoma palcami, możesz jednocześnie prowadzić i prowadzić rozmowę. Jedynym blokiem z jakim musimy się zmierzyć, to kichanie, podczas którego cała aktualna aktywność musi zawieszone na czas kichania. To dość irytujące, zwłaszcza gdy prowadzisz samochód i próbujesz podtrzymać rozmowę. Ty nie na pisanie kodu, który kicha.

Prawdopodobnie korzystasz z funkcji zdarzeń i wywołań zwrotnych, aby ominąć ten problem. Oto zdarzenia:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

To wcale nie kichać. Pobierzemy obraz, dodaję kilku słuchaczy, Wykonywanie kodu JavaScript może zostać przerwane, dopóki nie zostanie wywołany jeden z tych detektorów.

Niestety w powyższym przykładzie możliwe, że do zdarzenia miały miejsce przed rozpoczęciem ich odtwarzania. Musimy to jednak obejść, wykorzystując „zakończono” właściwość obrazów:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

Nie wychwytuje obrazów, które zawierają błędy, zanim mogliśmy wykryć them; DOM nie umożliwia nam tego. Oprócz tego wczytuję 1 obraz. Sprawa skomplikuje się jeszcze bardziej, jeśli chcemy wiedzieć, kiedy Liczba obrazów wczytanych: .

Wydarzenia nie zawsze są najlepszym sposobem

Wydarzenia świetnie sprawdzają się w przypadku zdarzeń, które mogą powtórzyć się wiele razy obiekt – keyup, touchstart itd. Te zdarzenia Cię nie interesują o tym, co się wydarzyło przed podłączeniem detektora. Ale jeśli chodzi o powodzenie/niepowodzenie asynchronicznie, najlepiej więc uzyskać taki wynik:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

Takie są obietnice, ale z lepszym nazewnictwa. Jeśli elementy graficzne HTML miały atrybut "gotowy" która zwróciła obietnicę, możemy to zrobić:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

W najprostszej wersji obietnice są podobne do detektorów zdarzeń z wyjątkiem tych kwestii:

  • Obietnica może się udać lub nie powieść tylko raz. Nie może wygrać ani dwa razy zaliczyć sukcesu, ani na odwrót.
  • informację o sukcesie lub niepowodzeniu danej obietnicy, a później dodasz do niej informację o sukcesie lub niepowodzeniu. wywołanie zwrotne, zostanie wykonane poprawne wywołanie zwrotne, nawet jeśli zdarzenie miejsce wcześniej.

Jest to szczególnie przydatne w przypadku powodzenia lub niepowodzenia asynchronicznego, ponieważ chcą sprawdzić, kiedy dokładnie coś się pojawiło, i są bardziej zainteresowane w reakcji na ten wynik.

Terminologia z obietnicą

Dowód Domenic Denicola przeczytaj pierwszą wersję roboczą otrzymał ocenę "F" terminologii. Uwięził mnie w więzieniu, zmuszono mnie do skopiowania Stany i losy 100 razy i napisałem zmartwiony list do moich rodziców. Mimo to terminologia jest mieszana, ale oto podstawowe informacje:

Obietnica może być:

  • fulfill – działanie związane z obietnicą zostało zrealizowane.
  • rejected – działanie powiązane z obietnicą nie powiodło się
  • pending (oczekuje) – nie został jeszcze zrealizowany ani odrzucony.
  • settled (rozstrzygnięto) – został zrealizowany lub odrzucony.

Specyfikacja Używa też terminu thenable, aby opisać obiekt przypominający obietnicę, tym, że ma metodę then. To określenie kojarzy mi się z dawną piłką nożną Menedżer Terry Venables: Postaram się go używać jak najczęściej.

W języku JavaScript pojawiają się obietnice!

Od dawna krążą obietnice w postaci bibliotek, takich jak:

To samo co obietnice JavaScript ma wspólne, ustandaryzowane zachowanie pod tytułem Promises/A+. Jeśli Jeśli jesteś użytkownikiem biblioteki jQuery, ma coś podobnego Odroczone. Pamiętaj jednak: Odroczone daty nie są zgodne z obietnicami/A+, co sprawia, że subtelnie różne i mniej przydatne, więc uważaj. Biblioteka jQuery ma też typ obietnicy, ale to tylko podzbiór danych „Odroczone” z tymi samymi problemami.

Mimo że implementacje obiecujące cechują się ustandaryzowanym zachowaniem, ogólnie różnią się od siebie. Obietnice JavaScript są podobne w interfejsie API do RSVP.js. Aby utworzyć obietnicę:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Konstruktor obietnic przyjmuje jeden argument, wywołanie zwrotne z dwoma parametrami, podjąć i odrzucić. Wykonaj działanie w wywołaniu zwrotnym (na przykład asynchronicznym), a następnie wywołaj jeśli wszystko się udało, w przeciwnym razie zadzwoń do odrzucenia.

Podobnie jak throw w starym języku JavaScript, funkcja ta, ale nie jest wymagana, odrzuć z obiektem Error. Zaletą obiektów błędów jest to, że przechwytują dzięki zrzutowi stosu narzędzia do debugowania.

Jak wykorzystać tę obietnicę:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

Funkcja then() przyjmuje 2 argumenty: wywołanie zwrotne w przypadku przypadku powodzenia i inny w przypadku błędu. Obie opcje są opcjonalne, więc możesz dodać wywołanie zwrotne do tylko w przypadku sukcesu lub niepowodzenia.

Obietnice JavaScript zaczynały się w interfejsie DOM jako „Futures”, które zostały przemianowane na „Promises”, a na koniec przenieśliśmy się do JavaScriptu. Stosowanie ich w JavaScript zamiast DOM jest świetny, ponieważ działa w kontekstach JS innych niż przeglądarki, np. Node.js (to, czy używa go w swoich podstawowych interfejsach API, to kolejne pytanie).

Chociaż są to funkcje JavaScript, DOM nie boi się ich używać. W Wszystkie nowe interfejsy DOM API z asynchronicznymi metodami powodzenie/niepowodzenia będą korzystać z obietnic. To samo dzieje się już u użytkowników Zarządzanie limitami, Zdarzenia wczytywania czcionek, ServiceWorker, Web MIDI, Strumienie i inne funkcje.

Zgodność z innymi bibliotekami

JavaScript obiecuje, że interfejs API JavaScript będzie traktować wszystko, co jest metodą then() jako (lub thenable w formie westchnienia), jeśli więc skorzystasz z biblioteki, zwraca obietnicę Q. Nie ma sprawy. Będzie dobrze działać z nowym Obietnice JavaScriptu.

Mimo że, jak wspomniałem/wspomniałam, funkcje Odroczone w jQuery są trochę... nieprzydatne. Na szczęście możesz rzucać je na standardowe obietnice, co warto zrobić. jak najszybciej:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

W tym przypadku właściwość $.ajax biblioteki jQuery zwraca wartość Deferred (odroczona). Ma metodę then(), Promise.resolve() może przekształcić to w obietnicę JavaScriptu. Pamiętaj jednak: czasami odraża przekazanie wielu argumentów do wywołania zwrotnego, na przykład:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

Z kolei JS obiecuje zignorowanie wszystkich elementów oprócz pierwszego:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

Na szczęście właśnie tego oczekujesz – a przynajmniej masz dostęp do swoje potrzeby. Pamiętaj też, że biblioteka jQuery nie jest zgodna z konwencją przekazywania obiektów Error do odrzuceń.

Łatwiejszy dostęp do złożonego kodu asynchronicznego

Dobrze, zakodujmy parę rzeczy. Chcemy:

  1. Uruchom wskaźnik postępu ładowania
  2. Pobierz plik JSON z artykułem, który zawiera tytuł i adresy URL każdego rozdziału
  3. Dodaj tytuł strony
  4. Pobierz każdy rozdział
  5. Dodaj opowiadanie do strony
  6. Zatrzymaj wskaźnik postępu

...oraz informować użytkownika, jeśli na drodze coś poszło nie tak. Chcemy aby w tym momencie wyłączyć wskaźnik ładowania. W przeciwnym razie zacznie się kręcić, gdy zakręci się w głowie i wystąpi jakiś inny interfejs.

Oczywiście nie trzeba używać JavaScriptu do przedstawiania historii, wyświetlanie stron w formacie HTML jest szybsze, W przypadku interfejsów API ten wzorzec jest dość powszechny. Wiele danych a potem wykonać jakąś czynność.

Na początek zajmijmy się pobieraniem danych z sieci:

Obiecywanie XMLHttpRequest

Stare interfejsy API zostaną zaktualizowane, aby korzystać z obietnic, jeśli to możliwe w sposób zgodny z przepisami. XMLHttpRequest to najlepszy kandydat, ale na razie napiszmy prostą funkcję wysyłającą żądanie GET:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

Teraz na przykład:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

Teraz można wysyłać żądania HTTP bez ręcznego wpisywania XMLHttpRequest. To świetne rozwiązanie, ponieważ mniej muszę oglądać irytujące wielbłądyXMLHttpRequest, tym szczęśliwsze będzie moje życie.

Łańcuch

then() to nie koniec opowieści. Możesz połączyć elementy then w powiązanie: przekształcania lub uruchamiania po kolei dodatkowych działań asynchronicznych.

Przekształcanie wartości

Możesz przekształcić wartości, zwracając im nową wartość:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

W praktycznym przykładzie wróćmy do:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

Odpowiedź jest w formacie JSON, ale obecnie otrzymujemy ją w postaci zwykłego tekstu. Śr może zmodyfikować naszą funkcję get, by korzystała z kodu JSON responseType ale możemy też rozwiązać ten problem w kraju obiecujących:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Ponieważ funkcja JSON.parse() przyjmuje jeden argument i zwraca przekształconą wartość, możemy utworzyć skrót:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

Praktycznie można bardzo łatwo przygotować funkcję getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse);
}

Funkcja getJSON() nadal zwraca obietnicę, która pobiera adres URL, a następnie go analizuje odpowiedź w formacie JSON.

Dodawanie działań asynchronicznych do kolejki

Możesz też połączyć elementy then w łańcuch, aby uruchamiać w sekwencji działania asynchroniczne.

Zwracanie czegoś z wywołania zwrotnego then() jest prawdziwą magią. Jeśli zwracasz wartość, jest wywoływane z nią kolejne działanie funkcji then(). Pamiętaj jednak: jeśli zwrócisz coś, co jest obiecujące, następny then() poczeka na to, a następnie wywoływana tylko wtedy, gdy obietnica zostanie ustanowiona (sukcesy/niepowodzenia). Na przykład:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Wysyłamy tutaj żądanie asynchroniczne do funkcji story.json, co daje zestaw danych o żądanie adresu URL, zwracamy uwagę na pierwszy z nich. Wtedy właśnie obiecują odróżniać się od prostych wzorców wywołań zwrotnych.

Możesz nawet stworzyć sposób, w jaki skróty pozwolą Ci dotrzeć do rozdziałów:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

Nie pobieramy typu story.json, dopóki nie zostanie wywołany getChapter, ale następnym razem czas(y) getChapter to inaczej, korzystamy z obietnicy tej historii, więc story.json jest pobierana tylko raz. Hurra!

Obsługa błędów

Jak widzieliśmy wcześniej, w funkcji then() przyjęto dwa argumenty – jeden za sukces, jeden za niepowodzenie (lub wykonanie i odrzucenie, zgodnie z obietnicą):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

Możesz też użyć catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() to nie tyle cukru, co cukier then(undefined, func), ale jest bardziej czytelna. Zwróć uwagę, że te dwa kody powyższe przykłady nie działają tak samo. Ten drugi jest odpowiednikiem:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

Różnica jest subtelna, ale niezwykle przydatna. Pomiń obietnicę odrzucenia do następnego elementu then() z wywołaniem zwrotnym odrzucenia (lub catch(), od odpowiednik). then(func1, func2), func1 lub func2 będą ani jedno, i drugie. Jednak w usłudze then(func1).catch(func2) obie , jeśli func1 odrzuca, ponieważ są to osobne kroki w łańcuchu. Wsiądź do: następujące:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

Powyższa procedura jest bardzo podobna do normalnego trybu try-catch JavaScript, przy czym występują błędy, występują w ramach „próby”. idź od razu do bloku catch(). Oto powyżej jako schematu blokowego (bo lubię schematy blokowe):

Niebieskie linie oznaczają spełnienie obietnic, a czerwone – te, które spełniają odrzuć.

Wyjątki i obietnice JavaScript

Odrzucenie następuje, gdy obietnica zostanie odrzucona bezpośrednio, ale również pośrednio jeśli w wywołaniu zwrotnym konstruktora zostanie zgłoszony błąd:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Oznacza to, że wszystkie zadania związane z obietnicami warto wykonywać w wywołanie zwrotne konstruktora obietnic, dzięki czemu błędy są automatycznie wychwytywane do odrzucenia.

To samo dotyczy błędów zgłoszonych w then() wywołaniach zwrotnych.

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Obsługa błędów w praktyce

W naszej historii i rozdziałach za pomocą tagu możemy wyświetlić użytkownikowi błąd:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Jeśli nie uda się pobrać adresu story.chapterUrls[0] (np. z powodu błędu http 500 lub użytkownik jest offline), pomija wszystkie kolejne udane wywołania zwrotne, w tym getJSON(), który próbuje przeanalizować odpowiedź jako JSON, ale także pomija wywołanie zwrotne, które dodaje fragment rozdział1.html do strony. Zamiast tego przesuwa się na haczyk. oddzwanianie. Z tego powodu wyświetla się komunikat „Nie udało się wyświetlić rozdziału”. zostaną dodane do strony, jeśli żadna z poprzednich czynności zakończyła się niepowodzeniem.

Podobnie jak w przypadku metody try/catch w przypadku JavaScriptu błąd jest wychwytywany, a kolejny kod więc wskaźnik postępu jest zawsze ukryty, a my tego chcemy. stanie się nieblokującą asynchroniczną wersją:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

Możesz catch() tylko do celów logowania, bez przywracania przed wystąpieniem błędu. Aby to zrobić, po prostu ponownie prześlij błąd. Możemy to zrobić w naszą metodę getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

Udało nam się pobrać jeden rozdział, ale chcemy go wszystkie. Zróbmy jakie zachodziły.

Równoległość i sekwencjonowanie: jak najlepsze wykorzystanie obu tych elementów

Myślenie asynchroniczne nie jest łatwe. Jeśli nie możesz odgadnąć, napisz kod tak, jakby był synchroniczny. W tym przypadku:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

To działa. Ale zostaje zsynchronizowana i blokuje przeglądarkę podczas pobierania. Do aby ta funkcja działała asynchronicznie, używamy funkcji then(), aby umożliwić sobie współdziałanie.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

Jak jednak zapętlić adresy URL rozdziałów i pobrać je po kolei? Ten nie działa:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach nie obsługuje aplikacji asynchronicznej, więc nasze rozdziały będą pojawiać się w dowolnej kolejności tak właśnie napisano Pulpfiction. To nie jest Pulpfiction, poprawmy to.

Tworzenie sekwencji

Chcemy przekształcić tablicę chapterUrls w sekwencję obietnic. W tym celu użyj narzędzia then():

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

Po raz pierwszy w przypadku zdarzenia Promise.resolve() tworzy się która osiąga wartość użytkownika. Jeśli przekażesz wystąpienia Promise zwróci po prostu je (uwaga: to do specyfikacji, których nie obsługują jeszcze niektóre implementacje). Jeśli przekazuje coś w stylu obietniczego (ma metodę then()), tworzy on Autentyczny Promise, który pełni/odrzuca tak samo. Jeśli zdasz w żadnej innej wartości, np. Promise.resolve('Hello'), tworzy w ramach obietnicy, która spełnia tę wartość. Jeśli wywołasz to bez wartości, tak jak powyżej, zostaje wypełnione wartością „nieokreśloną”.

Występuje też Promise.reject(val), który tworzy obietnicę odrzucającą warunki wartość, jaką mu podasz (lub niezdefiniowaną).

Powyższy kod możemy posprzątać za pomocą funkcji array.reduce:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

Możesz zrobić to tak samo jak w poprzednim przykładzie, ale nie trzeba dodawać osobnego „sekwencja” . Nasze wywołanie zwrotne o skróceniu jest wywoływane dla każdego elementu w tablicy. „sekwencja” jest Promise.resolve() za pierwszym razem, ale w pozostałej części wywołuje „sekwencję” to kwota, którą wróciliśmy z poprzedniego połączenia. array.reduce jest naprawdę przydatny przy sprowadzeniu tablicy do jednej wartości, która w tym przypadku jest obietnica.

Podsumujmy:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

I o to chodzi – w pełni asynchroniczną wersję wersji synchronizacji. Ale możemy . W tej chwili nasza strona jest pobierana w następujący sposób:

Przeglądarki potrafią pobierać wiele elementów naraz, więc tracimy i pobieraj rozdziały jeden po drugim. Nasz cel to: pobierać je wszystkie w tym samym czasie i przetwarzać po ich dostarczeniu. Na szczęście istnieje interfejs API do tego:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Funkcja Promise.all pobiera tablicę obietnic i tworzy obietnicę, która spełnia obietnice gdy wszystkie uda się rozwiązać. Otrzymujesz tablicę wyników (niezależnie od tego, realizowanych obietnic) w tej samej kolejności, w jakiej składane obietnice.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

W zależności od połączenia może to potrwać kilka sekund szybciej niż wczytywanie po kolei, Kod jest krótszy niż za pierwszym razem. Rozdziały można pobrać w dowolnym są wyświetlane na ekranie we właściwej kolejności.

Nadal możemy jednak poprawić postrzeganą skuteczność. Gdy pojawia się rozdział pierwszy, powinien dodać go do strony. Dzięki temu użytkownik będzie mógł zacząć czytać przed resztą pojawiły się rozdziały. Gdy pojawi się rozdział 3, nie dodamy go do ponieważ użytkownik może nie wiedzieć, że brakuje rozdziału 2. Kiedy rozdział drugi możemy dodać rozdział 2, 3 itd.

Aby to zrobić, jednocześnie pobieramy plik JSON dla wszystkich rozdziałów, a następnie tworzymy plik aby dodać je do dokumentu:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

I o to chodzi! Dostarczenie informacji zajmuje tyle samo czasu całą zawartość, ale użytkownik dostaje pierwszy fragment szybciej.

W tym prostym przykładzie wszystkie rozdziały pojawiają się mniej więcej w tym samym czasie, ale korzyści z wyświetlania pojedynczo będą przesadzone, gdy będzie ich więcej, rozdziałów.

wykonując powyższe czynności z wykorzystaniem wywołań zwrotnych w stylu Node.js lub wydarzenia są w pobliżu co najważniejsze, że kod nie jest tak łatwy. Jednak To nie koniec obietnic, ale w połączeniu z innymi funkcjami ES6 staje się jeszcze łatwiejsze.

Runda dodatkowa: większe możliwości

Od czasu, gdy pisałem ten artykuł, możliwość korzystania z obietnic się rozszerzyła bardzo. Od wersji Chrome 55 funkcje asynchroniczne umożliwiały kod oparty na obietnicach napisane tak, jakby były synchroniczne, ale bez blokowania wątku głównego. Dostępne opcje Więcej informacji znajdziesz w artykule o moich funkcjach asynchronicznych. Jest Powszechna obsługa funkcji Promises i asynchronicznych w najważniejszych przeglądarkach. Szczegółowe informacje można znaleźć w MDN Obietnica i asynchroniczne odwołania.

Dziękujemy Anne van Kesteren, Domenic Denicola, Tomowi Ashworthowi, Remyowi Sharpowi, Addy Osmani, Arthur Evans i Yutaka Hirano, którzy je sprawdzili poprawek/rekomendacji.

Podziękowania dla Mathiasa Bynensa za aktualizacji różnych części artykułu.