Asynchronous Programming Background
JavaScript машината е изградена върху идеята за еднонишков контур на събитие. Еднонишково означава, че само една част от кода се изпълнява в даден момент от време. Това е различно от други езици, като Java или C++, където няколко нишки могат да позволят множество различни части от код да се изпълняват едновременно. Поддържане и опазване на състоянието, когато няколко парчета код могат да получат достъп и променят това състояние е труден проблем и източник на чести грешки в софтуера.
Понеже JavaScript машината може да изпълнява само едно парче код в даден момент, е необходимо да се следи този код, който е предназначен да се изпълнява. Този код се съхранява в job queue. Всеки път, когато част от кода е готов да се изпълни той се добавя към job queue. Когато JavaScript машината завърши изпълнението на код, контура на събитието взема следващата задача от job queue и я изпълнява. Контура на събитието е процес вътре в JavaScript машината, който следи изпълнението на код и управлява job queue. Имайте в предвид, че в job queue изпълнението на работата започва да тече от първата задача до последната.
Модел на Events (събития)
Когато потребител кликне с мишката или натисне клавиш от клавиатурата, се задейства събитие (като onclick). Това събитие може да се използва, за да се отговори на взаимодействието чрез добавяне на нова задача, обратно в job queue. Това е най-основната форма на асинхронно програмиране в JavaScript: кода на манипулатора на събитието не се изпълнява, докато трае ефекта на събитието и когато се изпълни, той е с подходящ контекст. Например:
-
let button = document.getElementById("my-btn"); button.onclick = function(event) { console.log("Clicked"); };
В този код,
Събитията работят добре за прости взаимодействия, като това, но наредени верижно няколко отделни асинхронни събития е по сложно, защото вие трябва да следите целта на събитието за всяко събитие. Освен това, трябва да се гарантира, че всички подходящи манипулатори на събития са добавени преди самото възникване на събитието. Например, ако button в предишния пример е натиснат преди дефинирането на onclick, няма да се случи нищо. Така че, докато събитията са полезни за отговори на потребителски взаимодействия и подобна функционалност, която се проявява рядко, те не са много гъвкави за по-сложни нужди.
Моделът на Callbacks (обратно извикване)
Когато Node.js е създаден, той укрепва модела на асинхронното програмиране с популяризиране на модела за обратно извикване. Модела на обратното извикване е подобен на модела на събитието, тъй като не изпълнява код, до една по-късна точка във времето. Това е различно, защото функцията за извикване е подадена, като аргумент.
-
readFile("example.txt", function(err, contents) { if (err) { throw err; } console.log(contents); }); console.log("Hi!");
Този код използва традиционния Node.js стил за грешка при обратно извикване. Функцията readFile() е предназначена да чете от файл на диск (посочен, като първи аргумент) и след това изпълнява обратно извикване (като втори аргумент). Ако има грешка, err е обекта за грешка, в противен случай, contents съдържа съдържанието на файла, като string.
Използвайки модела на обратното извикване, readFile() започва изпълнението незабавно и спира, когато започва да чете от диска. Това означава, че console.log("Hi!") се извежда веднага след извикването на readFile() преди console.log(contents) да отпечата нещо. Когато readFile() приключи, той добавя нова задача в края на job queue с функцията за обратно извикване и нейните аргументи. Като тази работа се извършва след приключване на всички други задачи преди нея.
Моделът за обратно извикване е по-гъвкав от събития, защото е по-лесно да се наредят верижно няколко извиквания заедно. Например:
-
readFile("example.txt", function(err, contents) { if (err) { throw err; } writeFile("example.txt", function(err) { if (err) { throw err; } console.log("File was written!"); }); });
В този код, успешното извикване на readFile() води до друго асинхронно повикване, този път към writeFile() функцията. Обърнете внимание, че една и съща основна проверка err присъства и в двете функции. Когато readFile() завърши, той добавя работа в job queue, което води до writeFile(), която се извиква (ако няма грешки). След това, writeFile() добавя работа в job queue, когато тя завърши.
Въпреки, че това работи доста добре, можем бързо да влезем в модел, който е известен под името callback hell. Callback hell се случва, когато има твърде много вложени обратни извиквания:
-
method1(function(err, result) { if (err) { throw err; } method2(function(err, result) { if (err) { throw err; } method3(function(err, result) { if (err) { throw err; } method4(function(err, result) { if (err) { throw err; } method5(result); }); }); }); });
Влагането на няколко метода на извикване, както в този пример, създава заплетена мрежа от код, който е труден за разбиране и отстраняване на грешки. Обратните извиквания също създават проблеми, когато искате да постигнете по сложна функционалност. Какво става, ако искате две асинхронни операции да текат паралелно и да бъдете уведомени, когато и двете са изпълнени? И какво става, ако стартирате две асинхронни операции, но се взема резултата само на първата, която е завършила?
В тези случаи се налага да следите няколко обратни извиквания и операциите за почистване. Това е точно там, където Promises значително подобряват ситуацията.
Promises основи
Promise е контейнер за резултата на асинхронна операция. Вместо да се абонирате за дадено събитие или подадете обратно извикване към функция, функцията може да върне promise, като това:
-
// readFile обещава да завърши в някакъв момент в бъдеще let promise = readFile("example.txt");
В този код, readFile() всъщност не започва да чете файла незабавно, това ще се случи по-късно. Вместо това, функцията връща обект на promise, представляващ асинхронна операция за четене, така че да може да се работи с нея в бъдеще. Точно кога ще можете да работите с този резултат, зависи изцяло от това, как жизнения цикъл влияе на promise.
Жизнен цикъл на promise
Всяко promise преминава през кратък жизнен цикъл, който започва в изчакване на състоянието, което е показател, че асинхронната операция все още не е завършена. Изчакването на promise се счита за неуредено. Promise в последния пример е в състояние на изчакване, веднага след като функцията readFile() го върне. След като, асинхронната операция завърши, promise се счита за уредено и влиза в едно от двете възможни състояния:
-
- Fulfilled - promise на асинхронната операция завършва успешно.
- Rejected - promise на асинхронната операция не завършва успешно, поради грешка или някаква друга причина.
Вътрешно [[PromiseState]] свойството е настроено на "pending", "fulfilled" или "rejected" (изчакване, изпълнение или отхвърляне) за да отрази състоянието на promise. Това свойство не е изложено в обекта на promise, така че не можете да определите програмно, кое е състоянието на promise. Но можете да предприемете конкретно действие, когато promise променя състоянието с помощта на then() метода.
Метода then() е налице за всички promises и взема два аргумента. Първия аргумент е функция за извикване, когато promise е изпълнено. Всички допълнителни данни, свързани с асинхронната операция са подадени в изпълнението на тази функция. Вторият аргумент е функция за извикване, когато promise е отхвърлено. Подобно на функцията за изпълнение, във функцията за отхвърляне са подадени всички допълнителни данни, свързани с отхвърлянето.
И двата аргумента към then() не са задължителни, но с тях можете да слушате за всяка комбинация от изпълнението и отхвърлянето. Например, помислете за тази поредица от then() извиквания:
-
let promise = readFile("example.txt"); // слуша, както за изпълнение, така и за отхвърляне promise.then(function(contents) { // изпълнение console.log(contents); }, function(err) { // отхвърляне console.error(err.message); }); // слуша само за изпълнение - грешки не се отчитат promise.then(function(contents) { // изпълнение console.log(contents); }); // слуша само за отхвърляне - успех не се съобщава promise.then(null, function(err) { // отхвърляне console.error(err.message); });
Всичките три извиквания на then() работят върху едно и също promise. Първото извикване слуша, както за изпълнение така и за отхвърляне. Второто слуша само за изпълнение, грешки няма да бъдат докладвани. Третото просто слуша за отхвърляне и не докладва успех.
Promises също имат catch() метод, който се държи също, като then(), само когато се подава към манипулатор за отхвърляне. Например, следното извикване на catch() и then() e функционално еквивалентнo:
-
promise.catch(function(err) { // отхвърляне console.error(err.message); }); // е също като: promise.then(null, function(err) { // отхвърляне console.error(err.message); });
Целта е да се използва комбинацията от then() и catch(), за да се справят правилно с резултата на асинхронните операции. Тази система е по-добра от събития и обратни извиквания, защото прави операцията за успех или провал напълно ясна. (Събитията не са склонни да се изстрелват, когато има грешка и затова не трябва да забравяте във функцията за обратно извикване, винаги да проверявате за аргумента на грешка.) Ако не се прикачи манипулатор за отхвърляне към promise, всички неуспехи ще се случват мълчаливо. Добра идея е винаги да прикачвате манипулатор за отхвърляне, дори и само за да се регистрира провала.
Манипулаторите за изпълнение и отхвърляне ще бъдат изпълнени, дори ако се добавят към job queue, след като promise вече е уредено. Това ви позволява да добавите ново изпълнение и отхвърляне, работещи по всяко време и се гарантира, че те ще се извикат. Например:
-
let promise = readFile("example.txt"); // оригинално изпълнение на манипулатор promise.then(function(contents) { console.log(contents); // добавяне на друг нов promise.then(function(contents) { console.log(contents); }); });
В този код, манипулатора за изпълнение добавя друг манипулатор за изпълнение на същото promise. Promise вече е изпълнено в този момент, така че новия манипулатор за изпълнение се добавя в края на job queue и се извиква, когато е готов. Манипулаторите за отхвърляне работят по същия начин.
Създаване на неопределени promises
Нови promises се създават с помощта на Promise конструктора. Този конструктор приема един единствен аргумент, който е функция наречена изпълнител ( executor), съдържаща кода за инициализиране на promise. На изпълнителя се подават две функции, като аргументи resolve() и reject(). Функцията resolve() се извиква, когато изпълнителя е приключил успешно, за да сигнализира, че promise е готово да бъде resolved (разрешено), докато reject() функцията показва, че изпълнителя не е успял.
Ето един пример, който използва promise в Node.js за изпълнение на readFile() функцията от по-рано в тази глава.
-
// Node.js пример let fs = require("fs"); function readFile(filename) { return new Promise(function(resolve, reject) { // задейства асинхронна операция fs.readFile(filename, { encoding: "utf8" }, function(err, contents) { // проверява за грешки if (err) { reject(err); } // четенето успява resolve(contents); }); }); } let promise = readFile("example.txt"); // слуша, както за изпълнение, така и за отхвърляне promise.then(function(contents) { // изпълнение console.log(contents); }, function(err) { // отхвърляне console.error(err.message); });
В този пример, асинхронното извикване на натуралния Node.js fs.readFile() е увито в едно promise. Изпълнителя или подава обект за грешка към функцията reject() или съдържанието на файла към функцията resolve().
Имайте в предвид, че изпълнителя тръгва незабавно, когато се извика readFile(). Когато някое от resolve() или reject() се извика във вътрешността на изпълнителя, работата се добавя към job queue в поръчка за разрешаване на promise. Това се нарича job scheduling (работен график) и ако някога сте използвали setTimeout() или setInterval() функции, значи сте запознати с него. В работния график новата работа да се добавя в job queue, като се казва, “не я изпълнявай точно сега, но я изпълни по-късно". Например, setTimeout() функцията позволява да зададете закъснение преди добавяне на работата към job queue.
-
// добавяне на тази функция към job queue, след като изминат 500ms setTimeout(function() { console.log("Timeout"); }, 500) console.log("Hi!");
В този код, работния график се добавя към job queue след 500ms. Двете извиквания на console.log() извеждат следното:
-
Hi!
Timeout
Благодарение на закъснението от 500ms, изхода на функцията подадена към setTimeout() е показан след изхода от извикването на console.log("Hi!").
Promises работят по същия начин. Изпълнителя на promise се изпълнява незабавно, преди всичко, като след това се появява в изходния код. Например:
-
let promise = new Promise(function(resolve, reject) { console.log("Promise"); resolve(); }); console.log("Hi!");
Изхода на този код е:
-
Promise
Hi!
Извикването на resolve(), задейства асинхронна операция. Функциите подадени към then() и catch() се използват асинхронно, тъй като те също се добавят към края на job queue. Ето един пример:
-
let promise = new Promise(function(resolve, reject) { console.log("Promise"); resolve(); }); promise.then(function() { console.log("Resolved."); }); console.log("Hi!");
Изхода на този пример е:
-
Promise
Hi!
Resolved
Обърнете внимание, че въпреки, че извикването на then() се появява преди console.log("Hi!"), то всъщност не се изпълнява до по-късно (за разлика от executor). Това е, защото манипулаторите за изпълнение и отхвърляне, винаги се добавят към края на job queue, след като изпълнителя е завършил.
Създаване на уредени promises
Promise конструктора е най-добрият начин за създаване на неуредени promises, поради динамичния характер на това, което прави изпълнителя на promise. Но ако искате promise да представлява само една известна стойност, то тогава няма смисъл да минава през работния график за работа, който просто подава стойност към resolve() функцията. Вместо това има два метода, които създават уредени promises с дадена конкретна стойност.
Използване Promise.resolve ()
Метода Promise.resolve() приема един единствен аргумент и връща promise в изпълнено състояние. Това означава, че няма работен график, който да се изпълни и трябва да добавите един или повече манипулатора работещи с promise, за извличане на стойността. Например:
-
let promise = Promise.resolve(42); promise.then(function(value) { console.log(value); // 42 });
Този код създава изпълнено promise, така че манипулатора за изпълнение получава стойност 42. Ако се добави манипулатор за отхвърляне към това promise, той никога няма да се извика, защото promise никога няма да бъде в състояние на отхвърляне.
Използване Promise.reject ()
Можете също да създадете отхвърлени promises с помощта на Promise.reject() метода. Той работи по същия начин, както Promise.resolve() с изключение, на това, че създаденото promise е в състояние на отхвърляне, както следва:
-
let promise = Promise.reject(42); promise.catch(function(value) { console.log(value); // 42 });
Всички допълнителни манипулатори за отхвърляне добавени към това promise ще бъдат извикани, но не и манипулаторите за изпълнение.
Non-Promise Thenables
И двата, Promise.resolve() и Promise.reject() също приемат non-promise thenables, като аргументи. Когато подадем non-promise thenable, тези методи създават ново promise, което се извиква след then() функцията.
Non-promise thenable се създава, когато един обект има then() метод, който приема два аргумента: resolve и reject. Например:
-
let thenable = { then: function(resolve, reject) { resolve(42); } };
Обекта thenable в този пример, има характеристики свързани с promise, различни от then() метода. Можете да извикате Promise.resolve(), за да превърне thenable в изпълнено promise:
-
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
В този пример, Promise.resolve() извиква thenable.then(), така че състоянието на promise да може да се определи. Състоянието на promise за thenable е изпълнено, защото resolve(42) се извиква вътре в then() метода. Новото promise наречено p1 се създава в изпълнено състояние със стойността подадена от thenable (т. е. 42), така че манипулатора за изпълнение на p1 получава 42, като стойност.
Същият процес може да се използва с Promise.resolve(), за да създадете отхвърлено promise от thenable:
-
let thenable = { then: function(resolve, reject) { reject(42); } }; let p1 = Promise.resolve(thenable); p1.catch(function(value) { console.log(value); // 42 });
Този пример е подобен на последния с изключение на това, че thenable се отхвърля. Когато thenable.then() се изпълнява, се създава ново promise в отхвърлено състояние със стойност 42. Тази стойност след това се подава към манипулатора за отхвърляне на p1.
И двата Promise.resolve() и Promise.reject() работят по този начин за да може лесно да се работи с non-promise thenables. Много библиотеки използват thenables още преди promise да се въведат в ECMAScript 6, така че възможността да се преобразуват thenables във форма на promise е важно за обратна съвместимост с предишните съществуващи библиотеки. Когато не сте сигурни дали един обект е promise, подаването на обекта през Promise.resolve() или Promise.reject() (в зависимост от очаквания резултат) е най-добрия начин за разбиране, защото promises просто преминават непроменени.
Изпълнител на грешки
Ако се хвърли грешка вътре в изпълнителя, след това се извиква манипулатора за отхвърляне на promise. Например:
-
let promise = new Promise(function(resolve, reject) { throw new Error("Explosion!"); }); promise.catch(function(error) { console.log(error.message); // "Explosion!" });
В този код, изпълнителя умишлено хвърля грешка. Във всеки изпълнител има скрит try-catch блок, така че грешката се улавя и след това се подава на манипулатора за отхвърляне. В този смисъл. предишния пример е еквивалентен на този:
-
// еквивалентно на предишния пример let promise = new Promise(function(resolve, reject) { try { throw new Error("Explosion!"); } catch (ex) { reject(ex); } }); promise.catch(function(error) { console.log(error.message); // "Explosion!" });
Изпълнителя държи улавянето на всички хвърлени грешки, за да се опрости този случай на обща употреба, но хвърлена грешка в изпълнителя се съобщава, само когато присъства манипулатор за отхвърляне. В противен случай грешката не се съобщава. Това стана проблем за програмистите в началото на използването на promises, така че в JavaScript средите решиха да се справят с него, чрез използване на куки за улов на отхвърлени promises.
Global Promise Rejection Handling
Един от най-спорните аспекти на promises е мълчаливия отказ, което се случва, когато едно promise се отхвърли и не разполага с манипулатор за отхвърляне. Някои смятат, че това е най-големия недостатък в спецификацията, като това е единствената част на езика JavaScript, която очевидно не прави грешки, когато те се появят.
Определяне на, дали отхвърленото promise е обработено, не е лесно поради естеството на promises. Например, да вземем следния пример:
-
let rejected = Promise.reject(42); // в този момент, отхвърлянето не е обработено // известно време по-късно... rejected.catch(function(value) { // сега отхвърлянето е обработено console.log(value); });
Можете да извикате then() или catch() във всеки един момент от време и те да работят правилно независимо от това дали promise е уредено или не, което го прави трудно за разбиране, точно кога promise ще бъде обработено. В този случай promise се отхвърля веднага и не се обработва по-късно.
Въпреки, че е възможно следващата версия на ECMAScript да се справи с този проблем, браузърите и Node.js са приложили промени за справяне с този проблем. Имайте в предвид, че те не са част от спецификацията на ECMAScript 6, но са ценен инструмент при използването на promises.
Node.js Rejection Handling
В Node.js, има две събития в process обекта, свързани с обработката на отхвърлени promises.
-
- unhandledRejection- излъчва при отхвърлено promise и няма манипулатор за отхвърляне, извикан в рамките на един контур на цикъл на събитие
- rejectionHandled- излъчва при отхвърлено promise и има манипулатор за отхвърляне, извикано в рамките на един контур на цикъл на събитие
Тези две събития са проектирани да работят заедно, за подпомагане на идентифициране на promises, които са отхвърлени и не се обработват.
На манипулатора на събитието unhandledRejection се подава причина за отхвърлянето (често обекта на грешка) и promise, което е отхвърлено. Следният код показва unhandledRejection в действие:
-
let rejected; process.on("unhandledRejection", function(reason, promise) { console.log(reason.message); // "Explosion!" console.log(rejected === promise); // true }); rejected = Promise.reject(new Error("Explosion!"));
Този пример, създава отхвърлено promise с обекта на грешка и слуша за unhandledRejection събитието. Манипулатора на събитието получава обект на грешка, като първи аргумент и promise, като втори.
Манипулатора на събитието rejectionHandled получава само един аргумент, който е promise, което е отхвърлено. Например:
-
let rejected; process.on("rejectionHandled", function(promise) { console.log(rejected === promise); // true }); rejected = Promise.reject(new Error("Explosion!")); // изчаква добавяне на манипулатора за отхвърляне setTimeout(function() { rejected.catch(function(value) { console.log(value.message); // "Explosion!" }); }, 1000);
Тука, rejectionHandled събитието се излъчва, когато манипулатора за отхвърляне се извика най-накрая. Ако манипулатора за отхвърляне е прикрепен непосредствено след създаването на rejected, събитието няма да бъде излъчено. Манипулатора за отхвърляне се извиква по същото време с контура на цикъла на събитието, в което rejected е създаден, което не е позволено.
За правилно проследяване на потенциално необработени отхвърляния, използвайте unhandledRejection и rejectionHandled събитията, за да се поддържа списък на потенциално необработените отхвърляния. След това изчакайте известен период от време, за да се запознаят със списъка. Например:
-
let possiblyUnhandledRejections = new Map(); // когато, отхвърлянето е необработено го добавяме в map process.on("unhandledRejection", function(reason, promise) { possiblyUnhandledRejections.set(promise, reason); }); process.on("rejectionHandled", function(promise) { possiblyUnhandledRejections.delete(promise); }); setInterval(function() { possiblyUnhandledRejections.forEach(function(reason, promise) { console.log(reason.message ? reason.message : reason); // направи нещо за да се справят с тези отхвърляния handleRejection(promise, reason); }); possiblyUnhandledRejections.clear(); }, 60000);
Този код е просто необработено отхвърляне на tracker. Той използва map за съхраняване на promises и причините за тяхното отхвърляне. Всяко promise е ключ и причината за тази стойност. Всеки път, когато unhandledRejection излъчва, promise и неговата причина за отхвърляне, се добавят към map. Всеки път, когато rejectionHandled излъчва, обработеното promise се отстранява от map. В резултат на това, possiblyUnhandledRejections расте и да се свива при всяко извикване на събитие. Извикването на setInterval() периодично проверява списъка на възможните необработени отхвърляния и извежда информацията в конзолата (в действителност, вероятно ще искате да направите нещо друго, да влезете или по някакъв друг начин да се справите с отхвърлянето). В този пример използваме map вместо weak map, защото трябва да се запознаят с map периодично и да видят кои promises са налице, което не е възможно с weak map.
Докато този пример е специфичен за Node.js, браузърите също прилагат подобен механизъм за съобщаване на програмистите за необработени отхвърляния.
Browser Rejection Handling
Браузърите също излъчват две събития, за подпомагане на идентифицирането на необработени отхвърляния. Тези събития се излъчват от window обекта и са ефективно същите еквиваленти, като на Node.js:
-
- unhandledrejection- излъчва при отхвърлено promise и няма манипулатор за отхвърляне, извикан в рамките на един контур на цикъл на събитие
- rejectionhandled- излъчва при отхвърлено promise и има манипулатор за отхвърляне, извикан в рамките на един контур на цикъл на събитие
Докато прилагането на Node.js подава индивидуални параметри към манипулатора на събитието, манипулатора на събитие за тези събития получава обект на събитие със следните свойства:
-
- type - името на събитието ("unhandledrejection" или "rejectionhandled")
- promise - обекта на promise, което е било отхвърлено
- reason - стойността на отхвърляне от promise
Друга разлика в изпълнението на браузъра е, че стойността на отхвърляне ( reason) е разположение за двете събития. Например:
-
let rejected; window.onunhandledrejection = function(event) { console.log(event.type); // "unhandledrejection" console.log(event.reason.message); // "Explosion!" console.log(rejected === event.promise); // true }; window.onrejectionhandled = function(event) { console.log(event.type); // "rejectionhandled" console.log(event.reason.message); // "Explosion!" console.log(rejected === event.promise); // true }; rejected = Promise.reject(new Error("Explosion!"));
Този код възлага на двата манипулатора на събития да използват DOM Level 0 нотация за onunhandledrejection и onrejectionhandled (можете също да използвате addEventListener("unhandledrejection") и addEventListener("rejectionhandled") ако предпочитате). Всеки манипулатор получава обект на събитие, съдържащ информация за отхвърленото promise. Всички три свойства type, promise и reason, са на разположение и в двата манипулатора на събития.
Кода, който следи за необработени отхвърляния в браузъра е много подобен на кода за Node.js:
-
let possiblyUnhandledRejections = new Map(); // когато отхвърлянето е необработено, го добавяме в map window.onunhandledrejection = function(event) { possiblyUnhandledRejections.set(event.promise, event.reason); }; window.onrejectionhandled = function(event) { possiblyUnhandledRejections.delete(event.promise); }; setInterval(function() { possiblyUnhandledRejections.forEach(function(reason, promise) { console.log(reason.message ? reason.message : reason); // направи нещо за да се справят с тези отхвърляния handleRejection(promise, reason); }); possiblyUnhandledRejections.clear(); }, 60000);
Това изпълнение е почти точно същото, като изпълнението на Node.js. Той използва същия подход на съхраняване на promises и техните стойности за отхвърляне в map и след това ги инспектира по-късно. Единствената разлика е, когато информацията се извлича от манипулаторите на събитието.
Обработката на отхвърлени promise може да бъде трудна, но вие току що започнахте да виждате, колко мощни наистина могат да бъдат promises. Сега е време да предприемем следващата стъпка - верига от няколко promises заедно.
Верижни promises
До този момент promises може да изглеждат, като малко повече от частично подобрение с помощта на някаква комбинация от извикване и setTimeout() функцията, но има много повече за promises, от колкото виждаме. По-специално има редица начини за верига от promises заедно, за постигане на по-сложно асинхронно поведение.
Всяко извикване на then() или catch() всъщност създава и връща друго promise. Това второ promise е разрешено само, след като първото е изпълнено или отхвърлено. Например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); p1.then(function(value) { console.log(value); }).then(function() { console.log("Finished"); });
Резултата от този пример е:
-
42
Finished
Извикването към p1.then() връща второ promise, върху което се извиква then(). Манипулатора за изпълнение на втория then() се извиква само, след като първото promise е разрешено. Ако се освободим от този пример, това изглежда така:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = p1.then(function(value) { console.log(value); }) p2.then(function() { console.log("Finished"); });
В тази свободна версия на кода, резултат на p1.then() се съхранява в p2, след това p2.then() се извиква за да добави манипулатор за окончателното изпълнение. Както може би се досещате, p2.then() също връща promise. Този пример просто не използва това promise.
Прихващане на грешки
Верижните promises ни позволяват да улавяме грешки, които могат да възникнат по време на изпълнение или при манипулатора за отхвърляне от предишното promise. Например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); p1.then(function(value) { throw new Error("Boom!"); }).catch(function(error) { console.log(error.message); // "Boom!" });
В този пример, манипулатора за изпълнение за p1 хвърля грешка. Верижното извикване към catch() метода, който е във второто promise, може да получи тази грешка, чрез манипулатора за отхвърляне. Същото важи и ако един манипулатор за отхвърляне хвърли грешка:
-
let p1 = new Promise(function(resolve, reject) { throw new Error("Explosion!"); }); p1.catch(function(error) { console.log(error.message); // "Explosion!" throw new Error("Boom!"); }).catch(function(error) { console.log(error.message); // "Boom!" });
Тука, изпълнителя хвърля грешка, след това задейства манипулатора за отхвърляне на p1. Този манипулатор после хвърля друга грешка, която е уловена от манипулатора на второто отхвърлено promise. По този начин оковани извикванията на promises могат да бъдат информирани за грешки в други promises по веригата.
Връщане на стойност във верига от promises
Друг важен аспект на веригите от promises е способноста да се подават данни от едно promise към следващото. Вече видяхте, че стойността подадена към манипулатора resolve() вътре в изпълнителя, се подава към манипулатора за изпълнение на това promise. Можете да продължите подаването на данни по веригата, като посочите стойност за връщане от манипулатора за изпълнение. Например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); p1.then(function(value) { console.log(value); // "42" return value + 1; }).then(function(value) { console.log(value); // "43" });
В този пример, манипулатора за изпълнение на p1 връща стойност (value + 1), когато бъде изпълнен. Тъй като value е 42 (от изпълнителя), манипулатора за изпълнение връща 43. Тази стойност след това се подава към манипулатора за изпълнение на второто promise, което го извежда на конзолата.
Същото нещо е възможно с помощта на манипулатора за отхвърляне. Когато един манипулатор за отхвърляне се извика, той може да върне стойност. Тази стойност след това се използва, за да изпълни следващото promise във веригата. Например:
-
let p1 = new Promise(function(resolve, reject) { reject(42); }); p1.catch(function(value) { // първи манипулатор за изпълнение console.log(value); // "42" return value + 1; }).then(function(value) { // втори манипулатор за изпълнение console.log(value); // "43" });
Тука, изпълнителя извиква reject() с 42. Тази стойност се подава в манипулатора за отхвърляне на promise, където се връща value + 1. Въпреки, че тази върната стойност идва от манипулатора за отхвърляне, тя все още може да се използва в манипулатора за изпълнение на следващото promise във веригата. Това дава възможност на провала на едно promise да позволи възстановяване на цялата верига при необходимост.
Връщане на promises от вериги с promises
Връщането на примитивни стойности от манипулатора за изпълнение и отхвърляне, позволява прехвърляне на данни между promises, но какво става ако върне обект? Ако обекта е promise, има една допълнителна стъпка, която трябва да бъде взета, за да се определи, как да се процедира. Да разгледаме следния пример:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); p1.then(function(value) { // първи манипулатор за изпълнение console.log(value); // 42 return p2; }).then(function(value) { // втори манипулатор за изпълнение console.log(value); // 43 });
В този код, работния график на p1 разрешава 42. Манипулатора за изпълнение на p1 връща p2, promise вече е в разрешено състояние. Втория манипулатор за изпълнение се извиква, защото p2 е бил изпълнен. Ако p2 се отхвърли, ще се извика манипулатор за отхвърляне (ако има такъв) вместо втория манипулатор за изпълнение.
Важно е да се признае за този модел, че втория манипулатор за изпълнение не се добавя към p2, а по-скоро към трето promise. Следователно втория манипулатор за изпълнение е приложен към третото promise. Предишния пример е еквивалентен на това:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = p1.then(function(value) { // първи манипулатор за изпълнение console.log(value); // 42 return p2; }); p3.then(function(value) { // втори манипулатор за изпълнение console.log(value); // 43 });
От това става ясно, че втория манипулатор за изпълнение е приложен към p3, а не към p2. Това е малка, но важна разлика, тъй като втория манипулатор за изпълнение не се извиква, ако p2 се отхвърли. Например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { reject(43); }); p1.then(function(value) { // първи манипулатор за изпълнение console.log(value); // 42 return p2; }).then(function(value) { // втори манипулатор за изпълнение console.log(value); // никога не се извиква });
В този пример, втория манипулатор за изпълнение никога не се извиква, защото p2 се отхвърля. Може обаче да се добави манипулатор за отхвърляне, вместо него:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { reject(43); }); p1.then(function(value) { // първи манипулатор за изпълнение console.log(value); // 42 return p2; }).catch(function(value) { // манипулатор за отхвърляне console.log(value); // 43 });
Тука, манипулатора за отхвърляне се извиква в резултат на отхвърлянето на p2. Отхвърлената стойност 43 от p2 е подадена в манипулатора за отхвърляне.
Върнатата thenables от манипулатора за изпълнение и отхвърляне не се променя, когато се изпълняват изпълнителите на promise. Първото дефинирано promise ще изпълни своя изпълнител първо, последван от изпълнителя на второто promise и т.н. Върнатите thenables просто ви позволяват допълнителни отговори към резултатите на promise. Може да се отложи изпълнението на манипулатора за изпълнение, чрез създаване на ново promise в рамките на манипулатора за изпълнение. Например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); p1.then(function(value) { console.log(value); // 42 // създаване на ново promise let p2 = new Promise(function(resolve, reject) { resolve(43); }); return p2 }).then(function(value) { console.log(value); // 43 });
В този пример, се създава ново promise в рамките на манипулатора за изпълнение на p1. Това означава, че манипулатора на второто изпълнение няма да се изпълни, докато p2 не е изпълнено. Този модел е полезен, когато искате да изчакате предното promise да се уреди, преди да задействате друго promise.
Отговор на множество promises
До този момент, всеки пример тази глава се е занимавал с отговора на едно promise в даден момент. Има моменти обаче, когато ще искате да следите развитието на множество promises, за да се определи следващото действие. ECMAScript 6 предвижда два метода, които да следят множество promises: Promise.all() и Promise.race().
Метода Promise.all()
Метода Promise.all() приема един аргумент, който е iterable (също като array) на promises за наблюдение и връща promise, което е разрешено само, когато всяко promise в iterable е разрешено. Върнатото promise е изпълнено, когато се изпълни всяко promise в iterable, например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.all([p1, p2, p3]); p4.then(function(value) { console.log(Array.isArray(value)); // true console.log(value[0]); // 42 console.log(value[1]); // 43 console.log(value[2]); // 44 });
Всяко от promises в този пример разрешава номер. Извикването на Promise.all() създава ново promise p4, което в крайна сметка е изпълнено, когато всички promises са изпълнени. Резултата подаден към манипулатора за изпълнение на p4 е array, съдържащ всички разрешени стойности: 42, 43 и 44. Стойностите се съхраняват по реда решените promises, така могат да се сравнят резултатите на promises с promises, които ги решават.
Ако някое от promises подадени към Promise.all() е отхвърлено, върнатото promise се отхвърля незабавно, без да чака другите promises да завършат:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = new Promise(function(resolve, reject) { reject(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.all([p1, p2, p3]); p4.catch(function(value) { console.log(Array.isArray(value)) // false console.log(value); // 43 });
В този пример, p2 се отхвърля със стойност 43. Манипулатора за отхвърляне на p4 се извиква веднага, без да чака p1 или p3 да завършат изпълнението (те все още завършват изпълнението, просто p4 не чака).
Манипулатора за отхвърляне винаги получава една стойност, а не array, стойността е стойността на отхвърляне от promise, което е отхвърлено. В този случай, манипулатора за отхвърляне подава 43, за да отрази отхвърлянето от p2.
Метода Promise.race()
Метода Promise.race() осигурява малко по-различен поглед върху наблюдението на множество promises. Този метод също приема iterable на promises за наблюдение и връща promise, обаче, върнатото promise е уредено веднага, след като е уредено първото promise. Вместо да чака всички promises да бъдат изпълнени, както в Promise.all() метода, метода Promise.race() връща съответното promise веднага, след като някое от promises е изпълнено в array. Например:
-
let p1 = Promise.resolve(42); let p2 = new Promise(function(resolve, reject) { resolve(43); }); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.race([p1, p2, p3]); p4.then(function(value) { console.log(value); // 42 });
В този код, p1 е създаден, като изпълнено promise, докато другите са в работния график. Манипулатора за изпълнение на p4 се извиква после със стойност 42 и игнорира останалите promises напълно. Promises подадени към Promise.race() наистина участват в състезанието, за да се види, кое е уредено първо. Ако е изпълнено първото promise за уреждане, после върнатото promise е изпълнено, ако първото promise за уреждане е отхвърлено, тогава върнатото promise се отхвърля. Ето един пример с отхвърляне:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = Promise.reject(43); let p3 = new Promise(function(resolve, reject) { resolve(44); }); let p4 = Promise.race([p1, p2, p3]); p4.catch(function(value) { console.log(value); // 43 });
Тука, p4 се отхвърля, защото p2 е вече в отхвърлено състояние, когато Promise.race() се извиква. Въпреки, че p1 и p3 са изпълнени, техните резултати са игнорирани, тъй като те се появяват, след като p2 се отхвърли.
Наследяване от promises
Също като другите вградени типове, можете да използвате promises, като база за извлечен клас. Това ви позволява да определите свой вариант на promises, за разширяване на това, което вградените promises могат да направят. Да предположим, че искате да създадте едно promise, което използва success() и failure() в допълнение към then() и catch() методите. Можте да го направите, както следва:
-
class MyPromise extends Promise { // използва default конструктор success(resolve, reject) { return this.then(resolve, reject); } failure(reject) { return this.catch(reject); } } let promise = new MyPromise(function(resolve, reject) { resolve(42); }); promise.success(function(value) { console.log(value); // 42 }).failure(function(value) { console.log(value); });
В този пример, MyPromise произлиза от Promise и има два допълнителни метода. success() имитира метода resolve() и failure() имитира reject() метода.
И двата, success() и failure() използват this за извикване на методa, който имитират. Полученото promise функционира също, като вграденото promise, освен че сега може да извикате success() и failure() ако искате.
Тъй като, статичните методи са наследени, това означава, че MyPromise.resolve(), MyPromise.reject(), MyPromise.race() и MyPromise.all() методите, също присъстват в получените promises. Докато последните два метода се държат също, като вградените методи, първите два са малко по-различни.
И двата MyPromise.resolve() и MyPromise.reject() ще върнат инстанция на MyPromise независимо от подадената стойност, защото използват Symbol.species свойството (виж Глава 9) за да определят вида на promise за връщане. Така, че ако вграденото promise се подаде на двата метода, то ще бъде разрешено или отхвърлено и върнато в нов MyPromise, така че да може да се присвои от манипулатора за изпълнение и отхвърляне. Например:
-
let p1 = new Promise(function(resolve, reject) { resolve(42); }); let p2 = MyPromise.resolve(p1); p2.success(function(value) { console.log(value); // 42 }); console.log(p2 instanceof MyPromise); // true
Тука, p1 е вградено promise, което се подава на MyPromise.resolve(). Резултата p2 е инстанция на MyPromise, където стойността разрешена от p1 е подадена в манипулатора за изпълнение.
Ако една инстанция на MyPromise се подаде към MyPromise.resolve() или MyPromise.reject(), тя просто ще бъде върната директно без да бъде разрешена. Във всички други случаи, тези два метода се държат също, като Promise.resolve() и Promise.reject().
Asynchronous Task Running
Глава 8, въвежда генератори и ви показва как можете да ги използвате за асинхронни task running, като този:
-
let fs = require("fs"); function run(taskDef) { // създаване на итератор, предоставен на друго място let task = taskDef(); // начало на задачата let result = task.next(); // рекурсивна функция, която държи извикванията към next() function step() { // ако има още нещо за правене if (!result.done) { if (typeof result.value === "function") { result.value(function(err, data) { if (err) { result = task.throw(err); return; } result = task.next(data); step(); }); } else { result = task.next(result.value); step(); } } } // стартиране на процеса step(); } // Дефиниране на функция за използване на task runner function readFile(filename) { return function(callback) { fs.readFile(filename, callback); }; } // Пускане на задачата run(function*() { let contents = yield readFile("config.json"); doSomethingWith(contents); console.log("Done"); });
Има някои болни точки в това изпълнение. Първо, опаковането на всяка функция във функция, която връща функция е малко объркващо (дори това изречение е объркващо). Второ, няма начин да се направи разлика между върнатата стойност от функция, предназначена за обраното извикване на task runner и върнатата стойност, която не е за обратно извикване.
С promises, може значително да се опрости и обобщи този процес, като се гарантира, че всяка асинхронна операция връща promise. Този общ интерфейс означава, че може значително да се опрости асинхронния код. Ето един начин, с който бихте могли да опростите този task runner:
-
let fs = require("fs"); function run(taskDef) { // създаване на итератор let task = taskDef(); // начало на задачата let result = task.next(); // рекурсивна функция за обхождане (function step() { // ако има още нещо за правене if (!result.done) { // разрешаване на promise за лесно правене let promise = Promise.resolve(result.value); promise.then(function(value) { result = task.next(value); step(); }).catch(function(error) { result = task.throw(error); step(); }); } }()); } // Дефиниране на функция за използване на task runner function readFile(filename) { return new Promise(function(resolve, reject) { fs.readFile(filename, function(err, contents) { if (err) { reject(err); } else { resolve(contents); } }); }); } // Стартиране на задачата run(function*() { let contents = yield readFile("config.json"); doSomethingWith(contents); console.log("Done"); });
В тази версия на кода, функцията run() изпълнява генератор за създаване на итератор. Той извиква task.next() да стартиране на задачата и рекурсивно извиква step() докато итератора завърши.
Вътре в step() функцията, ако има още работа за вършене result.done е false. В този момент result.value трябва да бъде promise, но Promise.resolve() се използва само в случай, че въпросната функция не върне promise. (Не забравяйте, че Promise.resolve() само преминава през всяко promise, което и е подадено и ще обвие всяко не- promise в promise). После, се добавя манипулатор за изпълнение, който извлича стойността от promise и я подава обратно към итератора. След това, result присвоява следващия добит резултат преди самоизвикването на step().
Манипулатор за отхвърляне съхранява всички отхвърлени резултати в обект за грешка. Метода task.throw() подава този обект за грешка обратно в итератора и ако е уловена грешка в задачата, result присвоява следващия добит резултат. Накрая, се извиква step() вътре в catch(), за да продължи.
Тази функция run() може да работи с всеки генератор, който използва yield, като начин за използване на асинхронен код без да показва promises (или обратни извиквания) на програмиста. В действителност, тъй като върнатата стойност от извикването на функция винаги се преобразува в promise, функцията може да върне нещо различно от promise. Това означава, че можем да използваме синхронни и асинхронни методи, които да работят правилно, когато са извикани посредством yield и ние никога не трябва да проверяваме дали върнатата стойност е promise.
Единствената грижа е да се гарантира, че асинхронни функции, като readFile(), връщайки promise, правилно индентифицират състоянието му. За вградени методи на Node.js, означава, че трябва да се преобразуват тези методи, за да връщат promises, вместо да използват обратни извиквания.
Future Asynchronous Task Running
По време на моето писане, е в ход работата по привеждане на по-прост синтаксис за асинхронни task runner в JavaScript. Напредва работата по await синтаксиса, които ще е огледален на базирания на promises пример от предходния раздел. Основната идея е да се използва функция маркирана с async вместо генератор и използва await вместо yield при извикване на функция, като например:
(async function() { let contents = await readFile("config.json"); doSomethingWith(contents); console.log("Done"); });
Ключовата дума async преди function показва, че функцията е предназначена да работи по асинхронен начин. Ключовата дума await сигнализира, че извикването на функцията readFile("config.json") трябва да върне promise и ако това не стане, отговорът трябва да бъде опакован в promise. Точно както с изпълнението на run() в предходния раздел, await ще хвърли грешка, ако promise е отхвърлено и по друг начин да върне стойност от promise. Крайният резултат е, че може да се напише асинхронен код, като синхронен, без управлението на итератор-базирано състояние на машината.
Синтаксиса await се очаква да бъде финализиран в ECMAScript 2017 (ECMAScript 8).