Обещанията са гореща тема в Кръгове за разработка на JavaScript , и определено трябва да се запознаете с тях. Не е лесно да си увиете главата; може да отнеме няколко урока, примери и прилично количество практика, за да ги разберете.
Целта ми с този урок е да ви помогна да разберете обещанията на JavaScript и да ви подтикне да практикувате използването им повече. Ще обясня какво представляват обещанията, какви проблеми решават и как работят. Всяка стъпка, описана в тази статия, е придружена от jsbin
пример за код, който да ви помогне да работите заедно и да бъде използван като основа за по-нататъшно проучване.
Обещанието е метод, който в крайна сметка създава стойност. Може да се разглежда като асинхронен аналог на гетър функция. Същността му може да се обясни като:
promise.then(function(value) { // Do something with the 'value' });
Обещанията могат да заменят асинхронното използване на обратно извикване и те осигуряват няколко предимства пред тях. Те започват да се налагат, тъй като все повече и повече библиотеки и рамки ги възприемат като основен начин за справяне с асинхронността. Ember.js е чудесен пример за такава рамка.
Има няколко библиотеки които прилагат спецификацията Promises / A + . Ще научим основния речник и ще работим чрез няколко обещаващи примера на JavaScript, за да представим концепциите зад тях на практически начин. Ще използвам една от най-популярните библиотеки за внедряване, rsvp.js , в примерите за кода.
Пригответе се, ще хвърлим много зарове!
Обещанията и по този начин rsvp.js могат да се използват както на сървъра, така и от страна на клиента. За да го инсталирате за nodejs , отидете в папката на вашия проект и напишете:
npm install --save rsvp
Ако работите върху предния край и използвате bower, това е просто a
bower install -S rsvp
далеч.
Ако просто искате да влезете правилно в играта, можете да го включите чрез прост таг на скрипт (а с jsbin
можете да го добавите чрез падащото меню „Добавяне на библиотека“):
fulfill
Обещанието може да бъде в едно от трите състояния: в очакване , изпълнени , или отхвърлен . Когато е създадено, обещанието е в чакащо състояние. Оттук нататък той може да премине към изпълнено или отхвърлено състояние. Ние наричаме този преход „ разрешаване на обещанието . Разрешеното състояние на обещанието е крайното му състояние, така че след като бъде изпълнено или отхвърлено, то остава там.
Начинът да създадете обещание в rsvp.js е чрез това, което се нарича разкриващ конструктор . Този тип конструктор взема един параметър на функция и веднага го извиква с два аргумента, reject
и fulfilled
, които могат да прехвърлят обещанието към rejected
или var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
състояние:
then
Този модел на обещания в JavaScript се нарича разкриващ конструктор, тъй като аргументът с единична функция разкрива своите възможности на конструкторската функция, но гарантира, че потребителите на обещанието не могат да манипулират състоянието му.
Потребителите на обещанието могат да реагират на промените в неговото състояние, като добавят техния манипулатор чрез promise.then(onFulfilled, onRejected);
метод. Необходима е функция за обработка на изпълнение и отхвърляне, и двете могат да липсват.
onFulfilled
В зависимост от резултата от процеса на разрешаване на обещанието или onRejected
или function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
handler се извиква асинхронно .
Да видим пример което показва в какъв ред се изпълняват нещата:
1 2 3 Oh, noes, threw a 4.
Този фрагмент отпечатва изход, подобен на следния:
1 2 3 Yay, threw a 6.
Или, ако имаме късмет, виждаме:
then
Този обещаващ урок демонстрира две неща.
Първо, че манипулаторите, които прикачихме към обещанието, наистина бяха извикани, след като всички други кодове се изпълняваха асинхронно.
Второ, че манипулаторът за изпълнение е извикан само когато обещанието е изпълнено, със стойността, с която е разрешено (в нашия случай резултатът от хвърлянето на заровете). Същото важи и за манипулатора на отхвърляне.
Спецификацията изисква signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
функция (манипулаторите) също трябва да върне обещание, което дава възможност за обединяване на обещания заедно, което води до код, който изглежда почти синхронен:
signupPayingUser
Тук, function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log('Tossed a ' + toss + ', need to try again.'); return tossASix(); } function logSuccess(toss) { console.log('Yay, managed to toss a ' + toss + '.'); } function logFailure(toss) { console.log('Tossed a ' + toss + '. Too bad, couldn't roll a six'); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
връща обещание и всяка функция във веригата обещания се извиква с връщаната стойност на предишния манипулатор, след като приключи. За всички практически цели това сериализира повикванията, без да блокира основната нишка за изпълнение.
За да видим как всяко обещание се решава с връщаната стойност на предишния елемент във веригата, ние се връщаме към хвърляне на зарове. Искаме да хвърлим заровете най-много три пъти или докато не излязат първите шест jsbin :
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
Когато стартирате този обещаващ примерен код, ще видите нещо подобно на конзолата:
tossASix
Обещанието, върнато от logAndTossAgain
се отхвърля, когато хвърлянето не е шестица, така че манипулаторът на отхвърляне се извиква с действителното хвърляне. logAndTossAgain
отпечатва резултата на конзолата и връща обещание, което представлява друго хвърляне на зарове. Това отхвърляне от своя страна също се отхвърля и излиза от следващия Tossed a 4, need to try again. Yay, managed to toss a 6.
.
Понякога обаче имате късмет * и успявате да хвърлите шестица:
then
* Не е нужно да получавате че късметлия. Има около 42% шанс да хвърлите поне една шестица, ако хвърлите три зарове.
Този пример ни учи и на нещо повече. Вижте как не бяха направени повече хвърляния след първото успешно хвърляне на шестица? Имайте предвид, че всички манипулатори на изпълнение (първите аргументи в извикванията към null
) във веригата са logSuccess
, с изключение на последния, null
. Спецификацията изисква, ако манипулаторът (изпълнение или отхвърляне) не е функция, тогава върнатото обещание трябва да бъде разрешено (изпълнено или отхвърлено) със същата стойност. В горния пример за обещания манипулаторът за изпълнение, then
, не е функция и стойността на обещанието е изпълнена с 6. Така че обещанието, върнато от signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
обаждането (следващото във веригата) също ще бъде изпълнено с 6 като негова стойност.
Това се повтаря, докато не присъства действителен манипулатор на изпълнение (такъв, който е функция), така че изпълнението тече надолу докато се обработи. В нашия случай това се случва в края на веригата, където весело се излиза на конзолата.
Спецификацията Promises / A + изисква, ако дадено обещание бъде отхвърлено или грешка бъде хвърлена в манипулатора за отхвърляне, то трябва да бъде обработено от манипулатор на отхвърляне, който е „надолу по веригата“ от източника.
Използването на техниката за изтичане по-долу дава чист начин за справяне с грешки:
displayAndSendErrorReport
Тъй като манипулаторът за отхвърляне се добавя само в самия край на веригата, ако някой манипулатор на изпълнение във веригата бъде отхвърлен или изхвърли грешка, той се стича надолу, докато не се натъкне на var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log('Tossed a ' + tossWord.toUppercase() + '.'); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
Нека се върнем към любимите ни зарове и да видим това в действие. Да предположим, че просто искаме да хвърляме зарове асинхронно и да разпечатваме резултатите:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log('Tossed a ' + tossWord.toUpperCase() + '.'); } function logErrorMessage(error) { console.log('Oops: ' + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
Когато стартирате това, нищо не се случва. На конзолата нищо не се отпечатва и изглежда не се появяват грешки.
В действителност грешката се изхвърля, просто не я виждаме, тъй като по веригата няма манипулатори на отхвърляне. Тъй като кодът в манипулаторите се изпълнява асинхронно, с нов стек, той дори не излиза в конзолата. Нека да поправи това :
'Tossed a TWO.' 'Oops: Cannot read property 'toUpperCase' of undefined'
Изпълнението на горния код показва грешката сега:
logAndTossAgain
Забравихме да върнем нещо от undefined
и второто обещание е изпълнено с toUpperCase
. След това следващият манипулатор за изпълнение се взривява, опитвайки се да извика rsvp.js
на това. Това е друго важно нещо, което трябва да запомните: винаги връщайте нещо от манипулаторите или бъдете подготвени в следващите манипулатори, за да не мине нищо.
Сега видяхме основите на обещанията за JavaScript в примерния код на този урок. Голяма полза от използването им е, че те могат да бъдат съставени по прости начини, за да се получат „сложни“ обещания с поведението, което бихме искали. function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log('Rolled ' + result + ' with three dice.'); } function logErrorMessage(error) { console.log('Oops: ' + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
библиотеката предоставя шепа от тях и винаги можете да създадете свои собствени, използвайки примитивите и тези на по-високо ниво.
За последния, най-сложен пример, ние пътуваме до света на 500 г. сл. Н. Е ролева игра и хвърляне на зарове, за да получите резултати от герои. Такива резултати се получават чрез търкаляне три зарове за всяко умение на героя .
Нека да поставя кодът тук първо и след това обяснете какво е новото:
toss
Познати сме с RSVP.resolve
от последния пример за код. Това просто създава обещание, което винаги се изпълнява в резултат на хвърляне на зарове. Използвах threeDice
, удобен метод, който създава такова обещание с по-малко церемония (виж [1] в кода по-горе).
В RSVP.all
създадох 3 обещания, които представляват хвърляне на зар и накрая ги комбинирах с RSVP.all
. results
взема масив от обещания и се разрешава с масив от техните разрешени стойности, по едно за всяко съставно обещание, като същевременно поддържа техния ред. Това означава, че имаме резултата от хвърлянията в 'Rolled 11 with three dice'
(вижте [2] в кода по-горе) и ние връщаме обещание, което е изпълнено с тяхната сума (вижте [3] в кода по-горе).
Решаването на полученото обещание след това регистрира общия брой:
RSVP.all
Обещанията на JavaScript се използват за решаване на проблеми в приложения, които са много по-сложни от асинхронни хвърляния на зарове без основателна причина .
Ако заместите хвърлянето на три зарове с изпращане на три заявки ajax до отделни крайни точки и продължаване, когато всички те се върнат успешно (или ако някоя от тях се провали), вече имате полезно приложение на обещания и
|_+_|
Обещанията, когато се използват правилно, произвеждат лесен за четене код, по-лесен за разсъждение и по този начин по-лесен за отстраняване на грешки, отколкото обратно извикване. Не е необходимо да се създават конвенции относно, например, обработка на грешки, тъй като те вече са част от спецификацията.
Ние едва надраскахме повърхността на това, което обещанията могат да направят в този урок за JavaScript. Обещаващите библиотеки предоставят на ваше разположение добри дузина методи и конструктори от ниско ниво. Овладейте ги и небето е ограничението в това, което можете да правите с тях.
Balint Erdi отдавна беше страхотен фен на ролите и AD&D и сега е страхотно обещание и фен на Ember.js. Постоянното е неговата страст към рокендрола. Ето защо той реши да пише книга на Ember.js който използва рокендрола като тема на приложението в книгата. Регистрирайте се тук за да знаете кога стартира.