Другое

Как преобразовать Callback API в Promises в JavaScript

Узнайте, как преобразовать события DOM, колбэки в стиле Node и вложенные колбэки в промисы JavaScript. Полное руководство с примерами и лучшими практиками.

Как преобразовать существующие callback‑API в промисы в JavaScript?

Я хочу работать с промисами, но у меня есть callback‑API в различных форматах:

1. Callback‑однократных событий DOM (или других):

javascript
window.onload; // set to callback
...
window.onload = function() {
    // callback implementation
};

2. Простые callbacks:

javascript
function request(onChangeHandler) {
    // implementation
}
request(function() {
    // change happened
    // callback implementation
});

3. Node‑style callbacks (“nodebacks”):

javascript
function getStuff(dat, callback) {
    // implementation
}
getStuff("dataParam", function(err, data) {
    // nodeback implementation
});

4. Библиотеки с большим количеством вложенных callbacks:

javascript
API;
API.one(function(err, data) {
    API.two(function(err, data2) {
        API.three(function(err, data3) {
            // nested callbacks
        });
    });
});

Как я могу работать с этими API, используя промисы, и какие методы существуют для их «промисификации»?

Вы можете преобразовать callback‑API в промисы в JavaScript, используя различные техники в зависимости от формата API. Для событий DOM оборачивайте слушатели в промисы; для простых callback‑ов создавайте ручные обёртки; для Node‑стиля используйте util.promisify; а для вложенных callback‑ов применяйте async/await с Promise.all, чтобы избежать callback‑hell.

Содержание


Преобразование событий DOM

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

Ручная обёртка промиса для событий

javascript
// Преобразовать window.onload в промис
function onloadPromise() {
  return new Promise((resolve) => {
    window.onload = resolve;
  });
}

// Использование
onloadPromise().then(() => {
  console.log('DOM полностью загружен');
});

Слушатель события в промис

Согласно developer.mozilla.org, вы можете преобразовать события в промисы, используя шаблон слушателя событий:

javascript
// Универсальный конвертер события в промис
function eventToPromise(element, event) {
  return new Promise((resolve, reject) => {
    element.addEventListener(event, resolve, { once: true });
    // Опционально: добавить обработку ошибок при необходимости
  });
}

// Пример с DOMContentLoaded
function domContentLoadedPromise() {
  return eventToPromise(document, 'DOMContentLoaded');
}

// Использование
domContentLoadedPromise().then(() => {
  console.log('DOM готов');
});

Специальные случаи для API браузера

Некоторые API браузера имеют встроенные промис‑альтернативы:

javascript
// Преобразовать requestAnimationFrame в промис
const promiseRequestAnimationFrame = () => 
  new Promise((resolve) => window.requestAnimationFrame(resolve));

// Преобразовать requestIdleCallback в промис
const promiseRequestIdleCallback = () => 
  new Promise((resolve) => window.requestIdleCallback(resolve));

// Использование
const timestamp = await promiseRequestAnimationFrame();

Преобразование простых callback‑ов

Для простых callback‑ов, которые не следуют шаблону error‑first Node, необходимо создать ручные обёртки промисов.

Базовое преобразование callback в промис

javascript
// Оригинальная функция с callback
function request(onChangeHandler) {
  // реализация
  setTimeout(() => {
    onChangeHandler('change happened');
  }, 1000);
}

// Преобразованная версия
function promisifiedRequest() {
  return new Promise((resolve) => {
    request(resolve);
  });
}

// Использование
promisifiedRequest().then((result) => {
  console.log(result); // 'change happened'
});

Универсальная функция promisify

Как показано в блоге Zell Liew, вы можете создать универсальную функцию promisify:

javascript
function promisify(originalFn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      originalFn.apply(this, [...args, resolve]);
    });
  };
}

// Использование
const promiseRequest = promisify(request);
promiseRequest().then(result => {
  console.log(result);
});

Преобразование Node‑стиля callback‑ов

Node.js предоставляет встроенную поддержку преобразования callback‑ов в промисы с помощью util.promisify.

Использование util.promisify

Согласно документации Node.js, util.promisify принимает функцию, следуя общему шаблону error‑first callback, и возвращает версию, которая возвращает промисы.

javascript
const util = require('util');
const fs = require('fs');

// Оригинальное использование callback
fs.readFile('./package.json', (err, buf) => {
  if (err) throw err;
  console.log(buf.toString('utf8'));
});

// Преобразованная версия
const readFile = util.promisify(fs.readFile);
readFile('./package.json')
  .then(buf => console.log(buf.toString('utf8')))
  .catch(err => console.error(err));

Сложные callback‑ы Node‑стиля

Для callback‑ов с несколькими результатами, util.promisify автоматически оборачивает все аргументы callback (кроме ошибки) в массив:

javascript
// Оригинальная функция
function getStuff(dat, callback) {
  // реализация
  callback(null, 'result1', 'result2', 'result3');
}

// Преобразованная версия
const getStuffPromise = util.promisify(getStuff);
getStuffPromise('dataParam')
  .then(([result1, result2, result3]) => {
    console.log(result1, result2, result3);
  });

Преобразование методов объекта

При работе с методами объекта необходимо привязать правильный контекст:

javascript
const fs = require('fs');
const util = require('util');

// Преобразовать метод stat
const stat = util.promisify(fs.stat);

// Использование
stat('.')
  .then(stats => {
    console.log(stats.isDirectory());
  })
  .catch(err => {
    console.error('Error:', err);
  });

Преобразование вложенных callback‑ов

Для библиотек с большим количеством вложенных callback‑ов вы можете избежать callback‑hell, преобразовав их в промисы и используя async/await.

Ручное преобразование вложенных callback‑ов

javascript
// Оригинальный стиль с вложенными callback‑ами
API.one(function(err, data) {
  if (err) throw err;
  API.two(function(err, data2) {
    if (err) throw err;
    API.three(function(err, data3) {
      if (err) throw err;
      console.log(data, data2, data3);
    });
  });
});

// Преобразованная версия с async/await
async function processAPI() {
  try {
    const data = await promisify(API.one);
    const data2 = await promisify(API.two);
    const data3 = await promisify(API.three);
    console.log(data, data2, data3);
  } catch (err) {
    console.error('Error:', err);
  }
}

Использование Promise.all для независимых операций

Когда операции независимы, используйте Promise.all для лучшей производительности:

javascript
// Преобразовать все методы API в промисы
const apiOnePromise = promisify(API.one);
const apiTwoPromise = promisify(API.two);
const apiThreePromise = promisify(API.three);

// Запустить независимые операции параллельно
async function processAPIParallel() {
  try {
    const [data, data2, data3] = await Promise.all([
      apiOnePromise(),
      apiTwoPromise(),
      apiThreePromise()
    ]);
    console.log(data, data2, data3);
  } catch (err) {
    console.error('Error:', err);
  }
}

Смешанные последовательные и параллельные операции

javascript
async function mixedOperations() {
  try {
    // Последовательные операции, зависящие друг от друга
    const data = await apiOnePromise();
    const data2 = await apiTwoPromise(data);
    
    // Параллельные операции, не зависящие от предыдущих результатов
    const [data3, data4] = await Promise.all([
      apiThreePromise(),
      apiFourPromise()
    ]);
    
    console.log(data, data2, data3, data4);
  } catch (err) {
    console.error('Error:', err);
  }
}

Продвинутые техники promisification

Пользовательский promisify с несколькими результатами

Для более сложных сценариев вы можете создать пользовательские функции promisify, которые обрабатывают несколько параметров callback по‑разному:

javascript
// Пользовательский promisify, возвращающий объект вместо массива
function promisifyObject(originalFn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      originalFn.apply(this, [...args, (err, result1, result2) => {
        if (err) return reject(err);
        resolve({ result1, result2 });
      }]);
    });
  };
}

Преобразование API библиотек

Для целых библиотек вы можете создать promisified версии нескольких методов:

javascript
// Преобразовать весь объект API
const promisifiedAPI = {
  one: promisify(API.one),
  two: promisify(API.two),
  three: promisify(API.three)
};

// Использование
async function usePromisifiedAPI() {
  try {
    const data = await promisifiedAPI.one();
    const data2 = await promisifiedAPI.two(data);
    const data3 = await promisifiedAPI.three(data2);
    return { data, data2, data3 };
  } catch (err) {
    console.error('API Error:', err);
    throw err;
  }
}

Использование сторонних библиотек

Для сложных потребностей promisification рассмотрите использование проверенных библиотек, таких как Bluebird:

javascript
const Promise = require('bluebird');

// Преобразовать весь модуль сразу
const fs = Promise.promisifyAll(require('fs'));

// Использование
fs.readFileAsync('./file.txt', 'utf8')
  .then(content => console.log(content))
  .catch(err => console.error(err));

Лучшие практики и обработка ошибок

Обработка ошибок в promisified функциях

Всегда правильно обрабатывайте ошибки при преобразовании callback‑ов в промисы:

javascript
// Хорошая обработка ошибок
function promisifiedFunction() {
  return new Promise((resolve, reject) => {
    originalFunction((err, result) => {
      if (err) {
        reject(err); // Корректная передача ошибки
      } else {
        resolve(result);
      }
    });
  });
}

// Использование с try‑catch
async function safeUsage() {
  try {
    const result = await promisifiedFunction();
    console.log(result);
  } catch (err) {
    console.error('Error occurred:', err);
    // При необходимости обработать конкретные типы ошибок
    if (err.code === 'ENOENT') {
      console.log('Файл не найден');
    }
  }
}

Управление памятью

Будьте осторожны с слушателями событий и очисткой:

javascript
function eventPromise(element, event) {
  return new Promise((resolve) => {
    const handler = () => {
      element.removeEventListener(event, handler);
      resolve();
    };
    element.addEventListener(event, handler);
  });
}

Показатели производительности

  • Повторно используйте promisified функции для лучшей производительности
  • Используйте Promise.all для независимых операций
  • Избегайте ненужных цепочек промисов, когда достаточно простых промисов
javascript
// Хорошо: повторное использование promisified функций
const readFile = util.promisify(fs.readFile);

// Лучше: использовать Promise.all для чтения нескольких файлов
async function readMultipleFiles(files) {
  return Promise.all(files.map(file => readFile(file, 'utf8')));
}

Привязка контекста для методов объекта

При promisification методов объекта убедитесь в правильной привязке контекста:

javascript
const obj = {
  data: 'some data',
  callbackMethod: function(callback) {
    callback(null, this.data);
  }
};

// Правильная привязка
const promisifiedMethod = util.promisify(obj.callbackMethod.bind(obj));

// Использование
promisifiedMethod().then(result => {
  console.log(result); // 'some data'
});

Заключение

Ключевые выводы

  • События DOM можно преобразовать в промисы, оборачивая слушатели событий в конструкторы промисов
  • Простые callback‑ы требуют ручных обёрток с new Promise()
  • Node‑стиль callback‑ов можно автоматически преобразовать с помощью util.promisify()
  • Вложенные callback‑ы лучше обрабатывать с async/await и Promise.all(), чтобы избежать callback‑hell

Практические рекомендации

  1. Начинайте с util.promisify() для API Node.js — это самый эффективный метод
  2. Для браузерных API сначала проверьте наличие промис‑альтернатив
  3. Создавайте переиспользуемые promisified функции, а не преобразовывайте «на лету»
  4. Используйте синтаксис async/await для более чистого и читаемого кода
  5. Всегда реализуйте надёжную обработку ошибок в цепочках промисов

Связанные вопросы и ответы

  • Как обрабатывать ошибки в promisified функциях? Используйте блоки try‑catch с async/await или .catch() в цепочках промисов
  • Можно ли преобразовать любой callback в промис? Да, но некоторые сложные случаи могут потребовать пользовательских обёрток
  • Каков влияние на производительность от promisification? Минимальное для большинства случаев, но переиспользуйте promisified функции, когда это возможно

Следуя этим техникам, вы сможете эффективно работать с существующими callback‑API, используя современный подход на основе промисов, делая ваш код более поддерживаемым и читаемым, при этом полностью используя возможности асинхронной экосистемы JavaScript.

Авторы
Проверено модерацией
Модерация