Вступ до ES6 Promises, або чотири функції, які вам потрібно знати

Вже добре знайомі з обіцянками? Тоді одразу переходьте до підсумкового завдання.

Обіцянки допомагають зробити код чистішим, зменшити кількість залежностей від зовнішніх бібліотек і підготуватися до async і await в ES7. Розробники, які лають чи не використовують їх, не знають, що вони втрачають.

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

Отже, чому я маю вивчати проміси, знову?

Щоб продемонструвати проблему з функціями зворотного виклику, давайте зробимо деякі анімації без HTML та CSS.

Припустимо, ми хочемо зробити таке:

  • запустити деякий код
  • зачекати одну секунду
  • запустити інший код
  • зачекати ще секунду
  • потім запустити ще один код

Такий шаблон часто використовується в анімації CSS3. Давайте реалізуємо його за допомогою нашого вірного друга setTimeout. Код виглядатиме приблизно так:

Виглядає жахливо, чи не так? А уявіть собі на хвилину, що вам потрібно зробити 10 кроків, а не 3 - яку піраміду з відступів вам доведеться збудувати. Це настільки погано, що люди навіть вигадали спеціальну назву — callback hell. І такі піраміди з функцій зворотного виклику з'являються скрізь - в обробці HTTP запитів, під час роботи з базою даних, при анімації, при реалізації взаємодії між процесами та інших місцях. Але:

Вони не з'являються в коді, який використовує обіцянки.

Але що вони обіцяють?

Можливо, найпростіший спосіб розібратися з тим, як працюють обіцянки — порівняти їх із коллбеками. Є чотири основні відмінності:

1. Коллбеки єфункціями, обіцянки є об'єктами

Коллбекі — це просто функції, які виконуються у відповідь на якусь подію, наприклад, подію таймера або отримання відповіді від сервера. Будь-яка функція може стати коллбеком, і будь-який коллбек є функцією.

Обіцянки є об'єктами, які зберігають інформацію, відбулися певні події чи ні, а якщо відбулися — то й їхній результат.

2. Коллбеки передаються як аргументи, обіцянки повертаються

Коллбеки визначаються незалежно від зухвалої функції і передаються як аргументи. Функція, що викликає, зберігає коллбек і викликає його, коли відбувається певна подія.

Обіцянки створюються всередині асинхронних функцій і повертаються. Коли відбувається подія, асинхронна функція оновлює обіцянку, щоб повідомити про це світ.

3. Коллбеки обробляють успішне чи неуспішне завершення, обіцянки нічого не обробляють.

Коллбекі, як правило, викликаються з інформацією про те, успішно або неуспішно завершилася операція, і повинні бути в змозі обробити обидва варіанти.

Обіцянки нічого не обробляють за замовчуванням, обробники додаються пізніше.

4. Коллбеки можуть обробляти кілька подій, обіцянки пов'язані лише з однією подією.

Коллбеки можна викликати кілька разів у функціях, в які вони передані.

Обіцянки можуть представляти тільки одну подію - вони обертають або успішне її завершення, або неуспішне лише один раз.

Маючи це на увазі, розглянемо обіцянки більш детально.

Чотири функції, які потрібно знати

1. new Promise(fn)

Обіцянки ES6 є екземплярами вбудованого класу Promise, тастворюються шляхом виклику new Promise з однією функцією як аргумент. Наприклад:

Виклик new Promise негайно викличе функцію, передану як аргумент. Мета цієї функції полягає в інформуванні об'єкта Promise, коли подія, з якою він пов'язаний, буде завершено.

Для того, щоб зробити це, функція, яку ви передаєте в конструктор, може приймати два параметри, які є функціями — resolve і reject. Виклик resolve(value) позначить обіцянку як успішно завершену та викличе обробник успішного завершення. Виклик reject(error) викликає обробник неуспішного завершення. Не можна викликати обидві ці функції одночасно. Функції resolve і reject обидві приймають один аргумент, який містить дані про подію.

Вживемо це до нашого прикладу з анімацією. Наведений вище приклад використовує функцію setTimeout, яка приймає коллбек, — натомість ми хочемо повернути обіцянку. Конструктор new Promise дозволяє нам це зробити:

Відмінно, тепер у нас є обіцянка, яка резолвується за секунду. Я знаю, вам, ймовірно, не терпиться дізнатися, як зробити щось після секунди - ми повернемося до цього пізніше, коли будемо розглядати другу функцію promise.then.

Функція, яку ми передаємо в New Promise у наведеному прикладі, приймає лише параметр resolve, ми опустили параметр reject. Це тому, що setTimeout виконується завжди і, таким чином, немає сценарію, де ми могли б завершити його неуспішно.

Допустимо, ми хочемо перевірити, чи підтримується певна анімація браузером, і якщо анімація не підтримується, дізнатися про це заздалегідь, а не після таймууту. Якщо функція isAnimationSupported(step) перевірятиме піддерку, ми можемо реалізувати це за допомогою reject:

Нарешті, важливо відзначити,якщо у переданій функції виникне виняток, обіцянка буде автоматично позначена як неуспішна. Як результат буде передано об'єкт виключення, якби ви його передали функцію reject як аргумент.

Щоб краще зрозуміти це, можна припустити, що вміст кожної функції, яку ви передаєте в конструктор Promise, обертається в try/catch, наприклад, так:

Так. Тепер ви знаєте, як створити обіцянку. Але коли вона має, як додати обробники подій успіху/невдачі? Для цього ми використовуємо метод then.

2. promise.then(onResolve, onReject)

Метод promise.then(onResolve, onReject) дозволяє призначити обробники подій обіцянки. Залежно від аргументів, ви можете обробити подію успішного завершення, відмова, або обидва:

Не намагайтеся обробити помилки, що виникають у функції onResolve, у функції onError в тому ж виклику. Це не працює.

Якщо це все, що робить promise.then, він дійсно не має ніяких переваг перед функціями зворотного виклику. На щастя, це не так: обробники, передані в promise.then, не просто опрацьовують результат попередньої обіцянки — те, що вони повертають, передається в наступні обіцянки.

promise.then завжди повертає обіцянку

Це працює з числами, рядками та іншими типами:

Але що ще важливіше, це працює з іншими обіцянками — повернення обіцянки з обробника then передає цю обіцянку як значення, що повертається then. Це дозволяє реалізовувати ланцюжки обіцянок:

І, як ви бачите, ланцюжок обіцянок дозволяє уникнути піраміди з колбеків. Незалежно від того, скільки рівнів коллбеків, еквівалент на обіцянках буде плоским.

Чи можете ви тепер, використовуючи те, про що ми говориливище, змінити приклад з анімацією за допомогою функції delay? Для вашої зручності я повторюю перший приклад. Щоб перевірити своє рішення, наведіть курсор миші над полем нижче.

До цього все було досить просто, але є кілька складних моментів. Наприклад:

Обробник reject у функції promise.then повертає успішно завершену обіцянку.

Той факт, що обробники відмови повертають за промовчанням успішну обіцянку, змусив мене помучитися, коли я тільки вивчав цю тему — я не дозволю цьому статися з вами. Ось приклад того, за чим варто стежити:

Що виведеться у консолі? Перевірте відповідь, навівши курсор миші над полем нижче:

Якщо ви хочете обробити помилку, що виникає в reject, переконайтеся, що не просто повертаєте значення, а повертаєте відхилену обіцянку. Тобто. замість:

Використовуйте чарівний прийом, який повертає відхилену обіцянку із заданим значенням:

Крім того, ви можете залишити виняток, при цьому допоможе той факт, що

promise.then повертає винятки у відхилену обіцянку

Це означає, що ви можете в обробнику (успіху чи невдачі) повернути відхилену обіцянку, зробивши таке:

Приклад, який демонструє це рішення:

3. promise.catch(onReject)

Тут усе просто. promise.catch(handler) – це еквівалент promise.then(null, handler).

Ні, якщо серйозно, це все, що він робить.

Один з патернів - додавати catch в кінці кожного ланцюжка обіцянок. Повернемося наприклад з анімацією для демонстрації.

Припустимо, ми маємо три кроки анімації, з секундним відставанням між ними. Кожен крок може залишити виняток, - наприклад, через відсутність підтримки браузером - після кожного then ми додамо блок catch, в якому зробимо потрібнізміни, але без анімації.

Чи можете ви написати це, використовуючи функцію delay, за умови, що на кожному кроці анімації можна назвати функцію runAnimation(number), а як резерв можна викликати runBackup(number)? Перевіряти потрібно кожен крок окремо, а не все, на випадок, якщо бразуер все ж таки може виконати якісь із кроків. Щоб перевірити відповідь, наведіть курсор на блок нижче.

У наведеному вище прикладі цікава схожість між блоком try/catch та обіцянками. Деякі люди думають про спілкування як про відкладені блоки try/catch — я так не роблю, але гадаю, це не зашкодить.

Отже, це три функції, які потрібно знати, щоб використовувати обіцянки. Але щоб використовувати чудово, ви повинні знати четверту.

4. Promise.all([promise1, promise2, …])

Функція Promise.all справді дивовижна. Те, що завдавало стільки болю при реалізації на колбеках, що я навіть не наважився навести приклад, з її допомогою зробити дуже просто.

Що вона робить? Вона повертає обіцянку, яка успішна, якщо всі аргументи успішні, і відхилено, коли будь-який з його аргументів відхилено. У разі успіху результуюча обіцянка містить масив результатів кожної обіцянки, а у разі невдачі — помилку першої неуспішної обіцянки.

Навіщо це може бути корисно? Наприклад, якщо ми хочемо виконати дві анімації паралельно.

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

Або можна використовувати Promise.all.

Припустимо, у нас є три функції, перші дві parallelAnimation1() і parallelAnimation2() повертають обіцянки, коли анімація завершиться, а третя finalAnimation() має викликатися, коли завершаться перші дві. Реалізувати такулогіку ми можемо в такий спосіб:

Просто, чи не так?

Інші випадки для використання Promise.all — завантаження кількох HTTP-запитів одночасно, запуск кількох процесів одночасно або кілька одночасних запитів до бази даних. З Promise.all зробити все легко.

Перевірте свої знання

Тепер, коли ви знаєте чотири функції для роботи з обіцянками, перевіримо ваші знання.

  • Завантажити два файли із сервера
  • Витягти з них деякі дані
  • Використовувати, щоб завантажити третій файл з сервера
  • Показувати дані з третього файлу за допомогою alert() або викликати alert() з повідомленням про помилку.

Функції завантаження перших двох файлів повертають обіцянки, функція завантаження третього файлу, на жаль, вимагає функцію зворотного виклику.

Доступні такі функції:

  • initialRequestA() - повертає першу обіцянку A
  • initialRequesВ() - повертає другу обіцянку B
  • getOptionsFromInitialData(a, b) повертає аргумент options для функції finalRequest
  • finalRequest(options, callback) запитує третій файл із сервера, викликає callback(error, data) після виконання. Об'єкт data приймає значення undefined у разі помилки, об'єкт error набуває значення undefined у разі успіху (поширений патерн у node.js).

Ви можете перевірити знання тут, де я вже написав ці функції для вас.

Коли напишете своє рішення, ви можете перевірити його, навівши курсо миші на блок нижче.

Бонус: дві неймовірно корисні функції, які ви можете дізнатися з мінімальними зусиллями

Тепер, коли ви знаєте чотири функції, які потрібно знати, розглянемо дві функції, які неймовірно корисно знати.

Promise.resolve(value)

Функція Promise.resolve повертає обіцянку, яка успішно завершена з переданим значенням. Ця функція є еквівалентом наступного коду:

Promise.reject(value)

Функція Promise.reject(value) повертає неуспішну обіцянку з переданим значенням. Вона є еквівалентом наступного коду:

Це неймовірно корисна функція, якщо ви хочете обробити помилку, але не хочете повертати успішну обіцянку після цього.

У статті я навів чимало інформації. Ключ до запам'ятовування використовувати все це. Але якщо ви щось забудете, то шукати це за статтею незручно.

Тому я зробив шпаргалку. Її можна роздрукувати та повісити поруч із монітором або на дверях туалету.

Корисні посилання

Більше дізнатися про ES6 можна у моїй статті The Bits You'll Actually Use.

Бажаєте почитати більше про обіцянки? Вам може бути цікаво: