“Промісифікація” – це довге слово для простої трансформації. Це перетворення функції, яка приймає колбек та повертає проміс.
Такі перетворення часто необхідні в реальному житті, оскільки багато функцій та бібліотеки засновані на колбеках, а використання промісів зручніше, тому є сенс «промісифікувати» їх.
Для кращого розуміння розглянемо приклад.
Ми візьмемо loadScript(src, callback)
з розділу Введення: колбеки.
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Помилка завантаження скрипта ${src}`));
document.head.append(script);
}
// використання:
// loadScript('path/script.js', (err, script) => {...})
Функція завантажує скрипт використовуючи аргумент src
, а потім викликає callback(err)
у випадку помилки чи callback(null, script)
у випадку успішного завантаження. Це усім відоме використання колбеку, яке ми вже бачили.
Давайте промісифікуємо цю функцію.
Створимо нову функцію loadScriptPromise(src)
, яка робить те саме (завантажує скрипт), але повертає проміс замість використання колбеку.
Іншими словами, ми будемо передавати тільки src
(не callback
) і отримаємо проміс у відповіді, який поверне script
коли завантаження успішне, і помилку, якщо ні.
Реалізація такої функції:
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err);
else resolve(script);
});
});
};
// використання:
// loadScriptPromise('path/script.js').then(...)
Як ви можете бачити, нова функція – це обгортка оригінальної loadScript
функції. Вона викликає власний колбек, який працює з функціями проміса resolve/reject
.
Як бачимо, функція loadScriptPromise
добре вписується в асинхронну поведінку промісів.
На практиці нам, швидше за все, знадобиться промісифікувати не одну функцію, тому є сенс зробити для цього спеціальну «функцію-помічник».
Ми назвемо її promisify(f)
– вона приймає функцію для промісифікації f
та повертає функцію-обгортку.
function promisify(f) {
return function (...args) { // повертає функію-обгортку (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // наш спеціальний колбек для f (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
args.push(callback); // додаємо колбек у кінець аргументів f
f.call(this, ...args); // викликаємо оригінальну функцію
});
};
}
// використання:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
Код може виглядати дещо складним, але по суті він такий самий, як ми написали вище, промісифікуючи функцію loadScript
.
Виклик функції promisify(f)
поверне функцію-обгортку для f
(*)
. Ця обгортка повертає проміс і викликає оригінальну функцію f
, відстежуючи результат у спеціальному зворотному виклику (**)
.
В цьому випадку, promisify
припускає, що оригінальна функція очікує колбек тільки з двома аргументами (err, result)
. З таким результатом колбеку ви працюватимете найчастіше. В такому випадку наш колбек написаний і відпрацьовуватиме правильно.
Але що, якщо вихідна функція f
очікує колбек з більшою кількістю аргументів callback(err, res1, res2, ...)
?
Ми можемо покращити нашу функцію-помічник. Зробімо розширену версію promisify
.
- Викличмо функцію
promisify(f)
з одним аргументом, то вона повинна працювати як і раніше. - Викличмо функцію
promisify(f, true)
з двома аргументами, яка повинна повернути проміс, який поверне масив результатів з колбеку. Те ж саме повинно відбуватись для колбеку з багатьма аргументами.
// promisify(f, true) повинна повернути масив результатів
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // наш спеціальний колбек для f
if (err) {
reject(err);
} else {
// повернемо для всі результати колбека, якщо задано значення manyArgs === true
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
}
// використання:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...);
Як ви можете бачити, це по суті те саме, що й вище, але resolve
викликається лише з одним або з усіма аргументами залежно від того, чи є manyArgs
істинним.
Для більш екзотичних форматів колбека, наприклад, без err
: callback(result)
, ми можемо промісифікувати функції без помічника, «вручну».
Існують також модулі з більш гнучкою промісифікацією, наприклад, es6-promisify або вбудована функція util.promisify
в Node.js.
Промісифікація –- це чудовий підхід, особливо якщо ви будете використовувати async/await
(розглянемо пізніше в розділі Async/await), але вона не є повноцінно заміною будь-яких колбеків.
Пам’ятайте, проміс може мати лише один результат, але колбек технічно може викликатися скільки завгодно разів.
Тому промісифікація використовується для функцій, що викликають колбек лише один раз. Наступні виклики колбека будуть проігноровані.
Коментарі
<code>
, для кількох рядків – обгорніть їх тегом<pre>
, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)