Элегантное асинхронное программирование с помощью «промисов»

в 15:39, , рубрики: async, asynchronous, javascript, promise, асинхронность, обещания, Программирование, промисы, разработка, Разработка веб-сайтов

Доброго времени суток, друзья!

Представляю вашему вниманию перевод статьи «Graceful asynchronous programming with Promises» с MDN.

«Обещания» (промисы, promises) — сравнительно новая особенность JavaScript, которая позволяет откладывать выполнение действия до завершения выполнения предыдущего действия или реагировать на неудачное выполнение действия. Это способствует правильному определению последовательности выполнения асинхронных операций. В данной статье рассматривается, как работают обещания, как они используются в Web API, и как можно написать собственное обещание.

Условия: базовая компьютерная грамотность, знание основ JS.
Задача: понять, что такое обещания и как они используются.

Что такое обещания?

Мы кратко рассмотрели обещания в первой статье курса, здесь мы рассмотрим их более подробно.

По сути, обещание — это объект, представляющий промежуточное состояние операции — он «обещает», что результат будет возвращен в будущем. Неизвестно, когда именно операция будет завершена и когда будет возвращен результат, но существует гарантия того, что когда операция завершится, ваш код либо что-то сделает с результатом, либо изящно обработает ошибку.

Как правило, то, сколько времени занимает выполнение асинхронной операции (не слишком долго!), интересует нас меньше, чем возможность немедленного реагирования на ее завершение. И, разумеется, приятно сознавать, что выполнение остального кода не блокируется.

Одним из наиболее часто встречающихся обещаний являются Web API, возвращающие обещания. Давайте рассмотрим гипотетическое приложение для видеочата. В приложении есть окно со списком друзей пользователя, нажатие на кнопку рядом с именем (или аватаром) пользователя запускает видеовызов.

Обработчик кнопки вызывает getUserMedia(), чтобы получить доступ к камере и микрофону пользователя. С того момента, как getUserMedia() обращается к пользователю за разрешением (использовать устройства, какое из устройств использовать, если у пользователя несколько микрофонов или камер, только голосовой вызов, среди прочего), getUserMedia() ожидает не только решения пользователя, но и освобождения устройств, если они в настоящее время используются. Кроме того, пользователь может ответить не сразу. Все это может привести к большим задержкам во времени.

Если запрос на разрешение использования устройств делается из основного потока, браузер блокируется до завершения выполнения getUserMedia(). Это недопустимо. Без обещаний всё в браузере становится «некликабельным» до тех пор, пока пользователь не даст разрешения на использование микрофона и камеры. Поэтому вместо того, чтобы ждать решения пользователя и возвращать MediaStream для потока, созданного из источников (камеры и микрофона), getUserMedia() возвращает обещание, которое обрабатывает MediaStream, как только он становится доступным.

Код приложения-видеочата может выглядеть так:

function handle CallButton(evt){
    setStatusMessage('Calling...')
    navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(chatStream => {
        selfViewElem.srcObject = chatStream
        chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream))
        setStatusMessage('Connected')
    }).catch(err => {
        setStatusMessage('Failed to connect')
    })
}

Функция начинается с вызова setStatusMessage(), отображающей сообщение 'Calling...', которое служит индикатором того, что предпринимается попытка вызова. Затем вызывается getUserMedia(), запрашивающая поток, который содержит видео и аудио дорожки. Как только поток сформирован, устанавливается видео элемент для отображения потока из камеры, именуемого 'self view', аудио дорожки добавляются в WebRTC RTCPeerConnection, представляющий собой подключение к другому пользователю. После этого статус обновляется до 'Connected'.

Если getUserMedia() завершается неудачно, запускается блок catch. Он использует setStatusMessage() для отображения сообщения об ошибке.

Обратите внимание, что вызов getUserMedia() возвращается, даже если видео поток еще не получен. Даже если функция handleCallButton() вернула управление вызвавшему ее коду, как только getUserMedia() завершит выполнение, она вызовет обработчик. До тех пор, пока приложение не «поймет», что вещание началось, getUserMedia() будет находится в режиме ожидания.

Примечание: вы можете узнать об этом больше в статье «Сигналы и видео вызовы». В этой статье приводится более полный код, чем тот, который мы использовали в примере.

Проблема функций обратного вызова

Для понимания того, почему обещания являются хорошим решением, полезно обратиться к старому способу написания функций обратного вызова (callbacks), чтобы увидеть, в чем заключалась их проблематичность.

В качестве примера рассмотрим заказ пиццы. Успешный заказ пиццы состоит из нескольких шагов, которые должны выполняться по порядку, один после другого:

  1. Выбираем начинку. Это может занять некоторое время, если долго раздумывать, и завершиться неудачей, если передумать и заказать карри.
  2. Размещаем заказ. Приготовление пиццы занимает некоторое время и может завершиться неудачей, если в ресторане отсутствуют необходимые ингредиенты.
  3. Получаем пиццу и едим. Получение пиццы может завершиться неудачей, если, например, мы не можем оплатить заказ.

Псевдокод, написанный в старом стиле, с использованием функций обратного вызова, может выглядеть так:

chooseToppings(function(toppings){
    placeOrder(toppings, function(order){
        collectOrder(order, function(pizza){
            eatPizza(pizza)
        }, failureCallback)
    }, failureCallback)
}, failureCallback)

Такой код тяжело читать и поддерживать (его часто называют «адом функций обратного вызова» или «адом коллбэков»). Функцию failureCallback() приходится вызывать на каждом уровне вложенности. Существуют и другие проблемы.

Используем обещания

Обещания делают код чище, легче для восприятия и поддержки. Если мы перепишем псевдокод с использованием асинхронных обещаний, то получим следующее:

chooseToppings()
.then(function(toppings){
    return placeOrder(toppings)
})
.then(function(order){
    return collectOrder(order)
})
.then(function(pizza){
    eatPizza(pizza)
})
.catch(failureCallback)

Так намного лучше — мы видим, что происходит, мы используем один блок .catch() для обработки всех ошибок, функция не блокирует основной поток (поэтому мы можем играть в видеоигры в ожидании пиццы), каждая операция гарантированно выполняется после завершения предыдущей. Поскольку в каждом обещании возвращается обещание мы можем использовать цепочку из .then. Здорово, правда?

Используя стрелочные функции, псевдокод можно еще больше упростить:

chooseToppings()
.then(toppings =>
    placeOrder(toppings)
)
.then(order =>
    collectOrder(order)
)
.then(pizza =>
    eatPizza(pizza)     
)
.catch(failureCallback)

Или даже так:

chooseToppings()
.then(toppings => placeOrder(toppings))
.then(order => collectOrder(order))
.then(pizza => eatPizza(pizza))
.catch(failureCallback)

Это работает, потому что () => x идентично () => { return x }.

Можно даже сделать так (поскольку функции просто передают параметры, нам не нужна многослойность):

chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback)

Такой код сложнее читать и его нельзя использовать с более сложными конструкциями, чем в псевдокоде.

Примечание: псевдокод можно еще улучшить, используя async/await, о котором будет рассказываться в следующей статье.

В своей основе обещания похожи на «прослушиватели» событий, но с некоторыми отличиями:

  • Обещание выполняется только один раз (успех или провал). Оно не может выполниться дважды или переключиться с успеха на провал или наоборот после выполнения операции.
  • Если обещание завершилось, и мы добавим функцию обратного вызова для обработки его результата, то данная функция будет вызвана, несмотря на то, что событие произошло перед добавлением функции.

Базовый синтаксис обещания: реальный пример

Знать обещания важно, поскольку многие Web API используют их в функциях, выполняющих потенциально сложные задачи. Работа с современными веб технологиями предполагает использование обещаний. Позже мы научимся писать собственные обещания, а пока рассмотрим несколько простых примеров, которые можно встретить в Web API.

В первом примере мы используем метод fetch() для получения изображения из сети, метод blob() для преобразования содержимого тела ответа в объект Blob и отобразим этот объект внутри элемента <img>. Данный пример очень похож на пример из первой статьи, но мы сделаем его немного по-другому.

Примечание: следующий пример не будет работать, если вы просто запустите его из файла (т.е. с помощью file://URL). Запускать его нужно через локальный сервер или с помощью онлайн-решений, таких как Glitch или GitHub pages.

1. Прежде всего, загрузите HTML и изображение, которое мы будем получать.

2. Добавьте элемент <script> в конец <body>.

3. Внутри элемента <script> добавьте следующую строчку:

let promise = fetch('coffee.jpg')

Метод fetch(), принимающий URL изображения в качестве параметра, используется для получения изображения из сети. В качестве второго параметра мы можем указать объект с настройками, но пока ограничимся простым вариантом. Мы сохраняем обещание, возвращаемое fetch(), в переменной promise. Как отмечалось ранее, обещание — это объект, представляющий промежуточное состояние — официальное название данного состояния — ожидание (pending).

4. Для работы с результатом успешного завершения обещания (в нашем случае, когда возвращается Response) мы вызываем метод .then(). Функция обратного вызова внутри блока .then() (часто именуемая исполнителем (executor)) запускается только когда обещание завершается успешно и возвращается объект Response — говорят, что обещание завершилось (выполнилось, fullfilled). Response передается в качестве параметра.

Примечание. Работа блока .then() похожа на работу прослушивателя событий AddEventListener(). Он запускается только после того, как происходит событие (после выполнения обещания). Разница между ними состоит в том, что .then() может быть вызван лишь раз, в то время как прослушиватель рассчитан на многократный вызов.

Получив ответ, мы вызываем метод blob() для преобразования ответа в объект Blob. Это выглядит так:

response => response.blob()

… что является краткой записью для:

function (response){
    return response.blob()
}

ОК, довольно слов. Добавьте после первой строки следующее:

let promise2 = promise.then(response => response.blob())

5. Каждый вызов .then() создает новое обещание. Это очень полезно: поскольку blob() также возвращает обещание, мы можем обработать объект Blob, вызвав .then() второго обещания. Учитывая, что мы хотим делать что-то более сложное, чем вызывать метод и возвращать результат, нам необходимо обернуть тело функции в фигурные скобки (в противном случае, будет выброшено исключение):

Добавьте следующее в конец кода:

let promise3 = promise2.then(myBlob => {

})

6. Давайте заполним тело функции-исполнителя. Добавьте внутрь фигурных скобок следующие строчки:

let objectURL = URL.createObjectURL(myBlob)
let image = document.createElement('img')
image.src = objectURL
document.body.appendChild(image)

Здесь мы вызываем метод URL.createObjectURL(), передавая ему в качестве параметра Blob, который вернуло второе обещание. Получаем ссылку на объект. Затем мы создаем элемент <img>, устанавливаем ему атрибут src со значением ссылки на объект и добавляем в DOM, чтобы изображение отображалось на странице.

Если вы сохраните HTML и загрузите его в браузере, то увидите, что изображение отображается, как и ожидалось. Отличная работа!

Примечание. Вероятно, вы заметили, что эти примеры несколько надуманы. Вы могли бы обойтись без методов fetch() и blob() и присвоить <img> соответствующий URL, coffee.jpg. Мы пошли таким путем, чтобы продемонстрировать работу с обещаниями на простом примере.

Реагирование на неудачу

Мы кое-что забыли — у нас отсутствует обработчик ошибок на случай, если одно из обещаний потерпит неудачу (будет отклонено). Мы можем добавить обработчик ошибок с помощью метода .catch(). Давайте сделаем это:

let errorCase = promise3.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

Для того, чтобы увидеть это в действии, укажите неправильный URL изображения и перезагрузите страницу. В консоли появится сообщение об ошибке.

В данном случае, если мы не добавим блок .catch(), в консоль также будет выведено сообщение об ошибке. Однако .catch() позволяет обрабатывать ошибки так, как мы этого хотим. В реальном приложении ваш блок .catch() может попытаться повторно получить изображение или отобразить изображение по умолчанию, или попросить пользователя выбрать другое изображение или сделать что-то еще.

Примечание. Смотрите живое демо (исходный код).

Объединение блоков

В действительности подход, который мы использовали для написания кода, не является оптимальным. Мы намеренно шли таким путем, чтобы вы смогли понять, что происходит на каждом этапе. Как было показано ранее, мы можем объединять блоки .then() (и блок .catch()). Наш код может быть переписан следующим образом (также см. simple-fetch-chained.html на GitHub):

fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
    let objectURL = URL.createObjectURL(myBlob)
    let image = document.createElement('img')
    image.src = objectURL
    document.body.appendChild(image)
})
.catch(e => {
    console.log('There has been a problem with your fetch operation: ' + e.message)
})

Помните, что значение, возвращаемое обещанием, передается в качестве параметра следующей функции-исполнителю блока .then().

Примечание. Блоки .then()/.catch() в обещаниях являются асинхронными эквивалентами синхронного блока try...catch. Запомните: синхронный try...catch не будет работать в асинхронном коде.

Выводы по терминологии обещаний

Давайте подведем некоторые итоги и составим короткое руководство, которое вы сможете использовать в будущем. Для закрепления знаний рекомендуем вам прочитать предыдущий раздел несколько раз.

1. Когда обещание создано, говорят, что оно находится в состоянии ожидания.
2. Когда обещание возвращается, говорят, что оно завершилось (разрешилось):

  1. 1. Успешно завершенное обещание называют выполненным. Оно возвращает значение, которое может быть получено через цепочку из .then() в конце обещания. Функция-исполнитель в блоке .then() содержит значение, возвращенное обещанием.
  2. 2. Неудачно завершенное обещание называют отклоненным. Оно возвращает причину, сообщение об ошибке, приведшей к отклонению обещания. Эта причина может быть получена через блок .catch() в конце обещания.

Запускаем код после выполнения нескольких обещаний

Мы изучили основы использования обещаний. Теперь давайте рассмотрим более продвинутые возможности. Цепочка из .then() — это прекрасно, но что если мы хотим вызвать несколько блоков обещаний один за другим?

Вы можете это сделать с помощью стандартного метода с гениальным названием Promise.all(). Он принимает массив обещаний в качестве параметра и возвращает новое обещание только если и когда все обещания в массиве будут выполнены. Это выглядит примерно так:

Promise.all([a,b,c]).then(values => {
        ...
})

Если все обещания выполнятся, функции-исполнителю блока .then() в качестве параметра будет передан массив с результатами. Если хотя бы одно из обещаний не выполнится, весь блок будет отклонен.

Это может быть очень полезным. Представьте, что мы получаем информацию для динамического наполнения пользовательского интерфейса контентом на нашей странице. Во многих случаях, более разумным является получение всей информации и ее последующее отображение на странице, чем отображение информации по частям.

Давайте рассмотрим другой пример:
1. Загрузите шаблон страницы и поместите тег <script> перед закрывающим тегом </body>.

2. Загрузите исходные файлы (coffee.jpg, tea.jpg и description.txt) или замените их своими.

3. В скрипте мы сначала определим функцию, возвращающую обещания, которые мы передадим Promise.all(). Это легко будет сделать, если мы запустим Promise.all() после завершения трех операций fetch(). Мы можем сделать следующее:

let a = fetch(url1)
let b = fetch(url2)
let c = fetch(url3)

Promise.all([a, b, c]).then(values => {
    ...
})

Когда обещание выполнится, переменная «values» будет содержать три объекта Response, по одному от каждой завершенной операции fetch().

Однако мы этого не хотим. Для нас не имеет значения, когда операции fetch() завершатся. То, чего мы хотим на самом деле, это загруженные данные. Это означает, что мы хотим запустить блок Promise.all() после получения пригодных blob, представляющих изображения, и пригодных текстовых строк. Мы можем написать функцию, которая это делает; добавьте следующее в ваш элемент <script>:

function fetchAndDecode(url, type){
    return fetch(url).then(response => {
        if(type === 'blob'){
            return response.blob()
        } else if(type === 'text'){
            return response.text()
        }
    }).catch(e => {
        console.log('There has been a problem with your fetch operation ' + e.message)
    })
}

Это выглядит немного сложно, поэтому давайте пройдемся по коду шаг за шагом:
1. Прежде всего, мы объявляем функцию и передаем ей URL и тип получаемого файла.

2. Структура функции аналогична той, что мы видели в первом примере — мы вызываем функцию fetch() для получения файла по определенному URL и затем передаем файл в другое обещание, которое возвращает раскодированное (прочитанное) тело ответа. В предыдущем примере это всегда был метод blob().

3. Существует два отличия:

  • Во-первых, второе возвращаемое обещание зависит от типа значения. Внутри функции-исполнителя мы используем оператор if… else if для возврата обещания в зависимости от типа файла, которым нам нужно декодировать (в данном случае мы делаем выбор между blob и text, но пример может быть легко расширен для работы с другими типами).
  • Во-вторых, мы добавили ключевое слово «return» перед вызовом fetch(). В результате мы сначала запускаем всю цепочку, а затем финальный результат (т.е. мы получаем обещание, возвращаемое blob() или text(), как значение, возвращаемое функцией, которую мы объявили). По сути, оператор return передает результат вверх по цепочке.

4. В конце мы вызываем метод .catch() для обработки любых ошибок, которые могут возникнуть в обещаниях в массиве, переданном в .all(). Если одно из обещаний будет отклонено, блок catch даст нам знать, с каким обещанием возникла проблема. Блок .all() все равно выполнится, но не будет отображать результаты, при получении которых были ошибки. Если вы хотите, чтобы .all() был отклонен, добавьте в конце блок .catch().

Код внутри функции является асинхронным и основан на обещаниях, поэтому вся функция работает как обещание.

4. Далее мы вызываем нашу функцию три раза, чтобы начать процесс получения и декодирования изображений и текста, и помещаем каждое из обещаний в переменную. Добавьте следующее:

let coffee = fetchAndDecode('coffee.jpg', 'blob')
let tea = fetchAndDecode('tea.jpg', 'blob')
let description = fetchAndDecode('description.txt', 'text')

5. Далее мы объявляем блок Promise.all() для выполнения некоторого кода только после выполнения всех трех обещаний. Добавьте блок с пустой функцией-исполнителем внутри .then():

Promise.all([coffee, tea, description]).then(values => {

})

Вы можете видеть, что он принимает массив обещаний в качестве параметра. Исполнитель запустится только после выполнения всех трех обещаний; когда это случится, результат каждого обещания (декодированное тело ответа) будет помещен в массив, это можно представить как [coffee-results, tea-results, description-results].

6. Наконец, добавьте следующее в исполнитель (здесь мы используем довольно простой синхронный код для помещения результатов в переменные (создавая объекты URL из blob) и отображения изображений и текста на странице):

console.log(values)
// помещаем результат каждого обещания в отдельную переменную
let objectURL1 = URL.createObjectURL(values[0])
let objectURL2 = URL.createObjectURL(values[1])
let descText = values[2]

// отображаем изображения
let image1 = document.createElement('img')
let image2 = document.createElement('img')
image1.src = objectURL1
image2.src = objectURL2
document.body.appendChild(image1)
document.body.appendChild(image2)

// отображаем текст
let para = document.createElement('p')
para.textContent = descText
document.body.appendChild(para)

Сохраните изменения и перезагрузите страницу. Вы должны увидеть, что все компоненты пользовательского интерфейса загружаются, хотя выглядит это не очень привлекательно.

Примечание. Если у вас возникли трудности, вы можете сравнить вашу версию кода с нашей — вот живое демо и исходный код.

Примечание. В целях улучшения данного кода вы можете сделать цикл по отображаемым элементам, получая и декодируя каждый, и затем сделать цикл по результатам внутри Promise.all(), запуская разные функции для отображения каждого результата в зависимости от его типа. Это позволит работать с любым количеством элементов.

Кроме того, вы можете определять тип полученного файла без необходимости явного указания свойства type. Это, например, можно сделать с помощью response.headers.get('content-type') для проверки заголовка Content-Type протокола HTTP.

Запускаем код после выполнения/отклонения обещания

Часто вам может потребоваться выполнить код после завершения обещания, независимо от его выполнения или отклонения. Раньше нам приходилось включать одинаковый код и в блок .then(), и в блок .catch(), например:

myPromise
.then(response => {
    doSomething(response)
    runFinalCode()
})
.catch(e => {
    returnError(e)
    runFinalCode()
})

В современных браузерах доступен метод .finally(), позволяющий запускать код после завершения обещания, что позволяет избежать повторения и делает код более элегантным. Код из предыдущего примера может быть переписан следующим образом:

myPromise
.then(response => {
    doSomething(response)
})
.catch(e => {
    returnError(e)
})
.finally(() => {
    runFinalCode()
})

Вы можете увидеть использование данного подхода на реальном примере — живое демо promise-finally.html (исходный код). Он работает также, как Promise.all() из предыдущего примера, за исключением того, что мы добавляем finally() в конец цепочки в функции fetchAndDecode():

function fetchAndDecode(url, type){
    return fetch(url).then(response => {
        if(type === 'blob'){
            return response.blob()
        } else if(type === 'text'){
            return response.text()
        }
    }).catch(e => {
        console.log(`There has been a problem with your fetch operation for resource "${url}": ${e.message}`)
    }).finally(() => {
        console.log(`fetch attempt for "${url}" finished.`)
    })
}

Мы будем получать сообщение о завершении каждой попытки получить файл.

Примечание. then()/catch()/finally() — это асинхронный эквивалент синхронного try()/catch()/finally().

Пишем собственное обещание

Хорошая новость состоит в том, что вы уже умеете это делать. Когда вы объединяете несколько обещаний с помощью .then(), или комбинируете их для получения определенной функциональности, вы создаете собственные асинхронные и основанные на обещаниях функции. Возьмите к примеру нашу функцию fetchAndDecode() из предыдущего примера.

Объединение различных основанных на обещаниях API для создания определенной функциональности — это наиболее распространенный способ работы с обещаниями, который показывает гибкость и мощь современных API. Тем не менее, существует другой способ.

Конструктор Promise()

Собственное обещание можно создать с помощью конструктора Promise(). Это может потребоваться в ситуации, когда у вас есть код из старого асинхронного API, который вам нужно «промисифицировать». Это делается для того, чтобы иметь возможность одновременно работать как с существующим, старым кодом, библиотеками или «фреймворками», так и с новым основанным на обещаниях кодом.

Рассмотрим простой пример — здесь мы оборачиваем вызов setTimeout() в обещание — это запустит функцию через 2 секунды, что выполнит обещание (используя переданный вызов resolve()) со строкой «Success!».

let timeoutPromise = new Promise((resolve, reject) => {
    setTimeout(function(){
        resolve('Success!')
    }, 2000)
})

resolve() и reject() — это функции, которые вызываются для выполнения или отклонения нового обещания. В данном случае обещание выполняется со строкой «Success!».

Когда вы вызываете это обещание, вы можете добавить к нему блок .then() для дальнейшей работы со строкой «Success!». Так мы может вывести строку в сообщении:

timeoutPromise
.then((message) => {
    alert(message)
})

… или так:

timeoutPromise.then(alert)

Смотрите живое демо (исходный код).

Приведенный пример не очень гибок — обещание может только выполняться с простой строкой, у нас нет обработчика ошибок — reject() (на самом деле setTimeout() не нужен обработчик ошибок, поэтому в данном случае это не имеет значения).

Примечание. Почему resolve(), а не fulfill()? На данный момент ответ таков: это сложно объяснить.

Работаем с отклонением обещания

Мы можем создать отклоненное обещание с помощью метода reject() — также как resolve(), reject() принимает простое значение, но в отличие от resolve(), простым значением является не результат, а причина отклонения, т.е. ошибка, которая передается в блок .catch().

Давайте расширим предыдущий пример, добавив в него условие отклонения обещания, а также возможность получать другие сообщения, кроме сообщения об успехе.

Возьмите предыдущий пример и перепишите его так:

function timeoutPromise(message, interval){
    return new Promise((resolve, reject) => {
        if(message === '' || typeof message !== 'string'){
            reject('Message is empty or not a string')
        } else if(interval < 0 || typeof interval !== number){
            reject('Interval is negative or not a number')
        } else{
            setTimeout(function(){
                resolve(message)
            }, interval)
        }
    })
}

Здесь мы передаем функции два аргумента — сообщение и интервал (задержку во времени). В функции мы возвращаем объект Promise.

В конструкторе Promise мы осуществляем несколько проверок с помощью структур if… else:

  1. Во-первых, мы проверяем сообщение. Если оно пустое или не является строкой, тогда мы отклоняем обещание и сообщаем об ошибке.
  2. Во-вторых, мы проверяем временную задержку. Если она отрицательная или не является числом, тогда мы также отклоняем обещание и сообщаем об ошибке.
  3. Наконец, если оба аргумента в порядке, выводим сообщение через определенное время (интервал) с помощью setTimeout().

Поскольку timeoutPromise() возвращает обещание, мы можем добавить к ней .then(), .catch() и т.д., чтобы улучшить ее функциональность. Давайте сделаем это:

timeoutPromise('Hello there!', 1000)
.then(message => {
    alert(message)
}).catch(e => {
    console.log('Error: ' + e)
})

После сохранения изменений и запуска кода вы увидите сообщение спустя одну секунду. Теперь попробуйте передать пустую строку в качестве сообщения или отрицательное число в качестве интервала. Вы увидите сообщение об ошибке как результат отклонения обещания.

Примечание. Вы можете найти наш вариант данного примера на GitHub — custom-promise2.html (исходный код).

Более реалистичный пример

Рассмотренный нами пример позволяет легко понять концепцию, но он не является по-настоящему асинхронным. Асинхронность в примере имитируется с помощью setTimeout(), хотя он все равно показывает полезность обещаний для создания функций с разумным потоком операций, хорошей обработкой ошибок и т.д.

Одним из примеров полезного асинхронного приложения, в котором используется конструктор Promise(), является idb library (IndexedDB with usability) Jake Archibald. Эта библиотека позволяет использовать IndexedDB API, основанный на функциях обратного вызова API для хранения и извлечения данных на стороне клиента, написанный в старом стиле, совместно с обещаниями. Если вы посмотрите на основной файл библиотеки, то увидите, что в нем используются те же приемы, которые мы рассмотрели выше. Следующий код преобразует основную модель запросов, используемую многими методами IndexedDB, в модель, совместимую с обещаниями:

function promisifyRequest(request){
    return new Promise(function(resolve, reject){
        request.onsuccess = function(){
            resolve(request.result)
        }

        request.onerror = function(){
            reject(request.error)
        }
    })
}

Здесь добавляется парочка обработчиков событий, которые выполняют или отклоняют обещание в соответствующих случаях:

Заключение

Обещания являются отличным способом создания асинхронных приложений, когда мы не знаем, какое значение вернет функция или сколько времени это займет. Они позволяют работать с последовательностью асинхронных операций без использования глубоко вложенных функций обратного вызова, и они поддерживают стиль обработки ошибок, характерный для синхронного оператора try...catch.

Обещания поддерживаются всеми современными браузерами. Единственным исключением является Opera Mini и IE11 и его более ранние версии.

В данной статье мы рассмотрели далеко не все особенности обещаний, только самые интересные и полезные. Вы познакомитесь с остальными особенностями и техниками, если захотите более подробно изучить обещания.

Большинство современных Web API основаны на обещаниях, поэтому обещания необходимо знать. Среди таких Web API можно назвать WebRTC, Web Audio API, Media Capture and Streams и др. Обещания будут становится все более и более востребованными, так что их изучение и понимание является важным шагом на пути освоения современного JavaScript.

Благодарю за внимание.

Конструктивная критика приветствуется.

Все замечания будут учтены при редактировании статьи.

Автор: aio350

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js