16 липня 2023 р.

Промісифікація

“Промісифікація” – це довге слово для простої трансформації. Це перетворення функції, яка приймає колбек та повертає проміс.

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

Для кращого розуміння розглянемо приклад.

Ми візьмемо 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), але вона не є повноцінно заміною будь-яких колбеків.

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

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

Навчальна карта