Обещания JavaScript: руководство с примерами
Опубликовано: 2022-03-11Промисы — горячая тема в кругах разработчиков JavaScript, и вам обязательно стоит с ними познакомиться. Их нелегко уложить в голове; для их понимания может потребоваться несколько руководств, примеров и приличное количество практики.
Моя цель в этом руководстве — помочь вам понять промисы JavaScript и подтолкнуть вас к более практике их использования. Я объясню, что такое промисы, какие проблемы они решают и как они работают. Каждый шаг, описанный в этой статье, сопровождается примером кода jsbin
, который поможет вам работать и будет использоваться в качестве основы для дальнейшего изучения.
Что такое обещание JavaScript?
Обещание — это метод, который в конечном итоге создает значение. Его можно рассматривать как асинхронный аналог функции-получателя. Суть его можно объяснить так:
promise.then(function(value) { // Do something with the 'value' });
Промисы могут заменить асинхронное использование обратных вызовов и имеют ряд преимуществ по сравнению с ними. Они начинают набирать силу по мере того, как все больше и больше библиотек и фреймворков используют их в качестве основного способа обработки асинхронности. Ember.js — отличный пример такого фреймворка.
Существует несколько библиотек, реализующих спецификацию Promises/A+. Мы изучим базовую лексику и поработаем с несколькими примерами промисов JavaScript, чтобы на практике представить концепции, лежащие в их основе. В примерах кода я буду использовать одну из наиболее популярных библиотек реализации, rsvp.js.
Приготовьтесь, мы будем бросать много костей!
Получение библиотеки rsvp.js
Промисы и, следовательно, rsvp.js можно использовать как на стороне сервера, так и на стороне клиента. Чтобы установить его для nodejs , перейдите в папку проекта и введите:
npm install --save rsvp
Если вы работаете над интерфейсом и используете беседку, это просто
bower install -S rsvp
прочь.
Если вы просто хотите получить права в игре, вы можете включить его с помощью простого тега script (а с помощью jsbin
вы можете добавить его через раскрывающийся список «Добавить библиотеку»):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
Какими свойствами обладает обещание?
Обещание может находиться в одном из трех состояний: ожидание , выполнено или отклонено . При создании обещание находится в состоянии ожидания. Отсюда он может либо перейти в выполненное, либо в отклоненное состояние. Мы называем этот переход разрешением обещания . Разрешенное состояние обещания является его окончательным состоянием, поэтому, как только оно выполнено или отклонено, оно остается в нем.
Промис в rsvp.js можно создать с помощью так называемого раскрывающего конструктора. Этот тип конструктора принимает один параметр функции и немедленно вызывает его с двумя аргументами, fulfill
и reject
, которые могут переводить обещание либо в fulfilled
, либо в rejected
состояние:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
Этот шаблон промисов в JavaScript называется раскрывающим конструктором, потому что единственный аргумент функции раскрывает свои возможности функции-конструктору, но гарантирует, что потребители промиса не смогут манипулировать его состоянием.
Потребители промиса могут реагировать на изменения его состояния, добавляя свой обработчик через метод then
. Требуется функция выполнения и функция обработчика отклонения, обе из которых могут отсутствовать.
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');
Этот фрагмент выводит вывод, аналогичный следующему:
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
возвращает промис, и каждая функция в цепочке промисов вызывается с возвращаемым значением предыдущего обработчика после его завершения. Для всех практических целей это сериализует вызовы, не блокируя основной поток выполнения.
Чтобы увидеть, как каждое обещание разрешается с возвращаемым значением предыдущего элемента в цепочке, мы вернемся к бросанию костей. Мы хотим бросить кости максимум три раза, или пока не выпадут первые шесть jsbin:
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
Когда вы запустите этот пример кода обещаний, вы увидите что-то вроде этого в консоли:
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.
* Вам не обязательно так повезло. Существует ~42% шанс выбросить хотя бы одну шестерку, если вы бросите три кубика.
Этот пример также учит нас чему-то большему. Видите, после первого удачного выпадения шестерки больше не было бросков? Обратите внимание, что все обработчики выполнения (первые аргументы в вызовах then
) в цепочке имеют значение null
, кроме последнего, logSuccess
. Спецификация требует, чтобы, если обработчик (выполнения или отклонения) не был функцией, возвращаемое обещание должно быть разрешено (выполнено или отклонено) с тем же значением. В приведенном выше примере промисов обработчик выполнения null
не является функцией, а значение промиса было выполнено с 6. Таким образом, промис, возвращаемый вызовом then
(следующим в цепочке), также будет выполнен. с 6 в качестве его значения.
Это повторяется до тех пор, пока не появится фактический обработчик выполнения (тот, который является функцией), поэтому выполнение просачивается вниз, пока не будет обработано. В нашем случае это происходит в конце цепочки, где он бодро выходит из консоли.
Обработка ошибок
Спецификация Promises/A+ требует, чтобы, если обещание было отклонено или в обработчике отклонения возникла ошибка, она должна быть обработана обработчиком отклонения, который находится «ниже по течению» от источника.
Использование приведенной ниже техники просачивания вниз дает чистый способ обработки ошибок:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
Поскольку обработчик отклонения добавляется только в самый конец цепочки, если какой-либо обработчик выполнения в цепочке отклоняется или выдает ошибку, он просачивается вниз, пока не наткнется на 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
. Это еще одна важная вещь, которую следует помнить: всегда возвращайте что-то из обработчиков или будьте готовы к тому, что в последующих обработчиках ничего не будет передано.
Здание выше
Теперь мы рассмотрели основы обещаний JavaScript в примере кода этого руководства. Большим преимуществом их использования является то, что они могут быть составлены простыми способами для создания «составных» обещаний с желаемым поведением. Библиотека rsvp.js
предоставляет несколько из них, и вы всегда можете создать свою собственную, используя примитивы и эти высокоуровневые.
В качестве последнего, самого сложного примера мы отправимся в мир ролевых игр AD&D и подбросим кости, чтобы получить очки персонажей. Такие очки можно получить, бросив три кубика за каждое умение персонажа.
Позвольте мне сначала вставить сюда код, а затем объяснить, что нового:
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);
Мы знакомы с toss
из последнего примера кода. Он просто создает обещание, которое всегда выполняется с результатом броска костей. Я использовал RSVP.resolve
, удобный метод, который создает такое обещание с меньшими церемониями (см. [1] в приведенном выше коде).
В threeDice
я создал 3 обещания, каждое из которых представляет собой бросок кости, и, наконец, объединил их с RSVP.all
. RSVP.all
принимает массив промисов и разрешается массивом их разрешенных значений, по одному для каждого составного промиса, сохраняя при этом их порядок. Это означает, что у нас есть результат бросков results
(см. [2] в приведенном выше коде), и мы возвращаем обещание, которое выполняется с их суммой (см. [3] в приведенном выше коде).
Разрешение полученного обещания затем регистрирует общее число:
"Rolled 11 with three dice"
Использование обещаний для решения реальных проблем
Обещания JavaScript используются для решения проблем в приложениях, которые намного сложнее, чем асинхронные броски костей без уважительной причины .
Если вы замените бросание трех игральных костей отправкой трех запросов ajax на отдельные конечные точки и продолжите работу, когда все они успешно вернутся (или если какой-либо из них не удался), вы уже получите полезное применение обещаний и RSVP.all
.
Промисы при правильном использовании создают легко читаемый код, о котором легче рассуждать и, следовательно, легче отлаживать, чем обратные вызовы. Нет необходимости устанавливать соглашения, касающиеся, например, обработки ошибок, поскольку они уже являются частью спецификации.
В этом руководстве по JavaScript мы едва коснулись того, что могут делать промисы. Библиотеки Promise предоставляют в ваше распоряжение добрую дюжину методов и низкоуровневых конструкторов. Освойте их, и небо — это предел того, что вы можете с ними сделать.
Об авторе
Балинт Эрди был большим поклонником ролевых игр и AD&D давным-давно, а сейчас является многообещающим поклонником Ember.js. Что было постоянным, так это его страсть к рок-н-роллу. Вот почему он решил написать книгу о Ember.js, в которой рок-н-ролл используется в качестве темы приложения в книге. Зарегистрируйтесь здесь, чтобы узнать, когда он будет запущен.