Как преобразовать Callback API в Promises в JavaScript
Узнайте, как преобразовать события DOM, колбэки в стиле Node и вложенные колбэки в промисы JavaScript. Полное руководство с примерами и лучшими практиками.
Как преобразовать существующие callback‑API в промисы в JavaScript?
Я хочу работать с промисами, но у меня есть callback‑API в различных форматах:
1. Callback‑однократных событий DOM (или других):
window.onload; // set to callback
...
window.onload = function() {
// callback implementation
};
2. Простые callbacks:
function request(onChangeHandler) {
// implementation
}
request(function() {
// change happened
// callback implementation
});
3. Node‑style callbacks (“nodebacks”):
function getStuff(dat, callback) {
// implementation
}
getStuff("dataParam", function(err, data) {
// nodeback implementation
});
4. Библиотеки с большим количеством вложенных callbacks:
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
- Преобразование простых callback‑ов
- Преобразование Node‑стиля callback‑ов
- Преобразование вложенных callback‑ов
- Продвинутые техники promisification
- Лучшие практики и обработка ошибок
Преобразование событий DOM
Для одноразовых событий, таких как window.onload или другие события DOM, вы можете преобразовать их в промисы, оборачивая логику слушателя в конструктор промиса.
Ручная обёртка промиса для событий
// Преобразовать window.onload в промис
function onloadPromise() {
return new Promise((resolve) => {
window.onload = resolve;
});
}
// Использование
onloadPromise().then(() => {
console.log('DOM полностью загружен');
});
Слушатель события в промис
Согласно developer.mozilla.org, вы можете преобразовать события в промисы, используя шаблон слушателя событий:
// Универсальный конвертер события в промис
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 браузера имеют встроенные промис‑альтернативы:
// Преобразовать 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 в промис
// Оригинальная функция с 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:
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, и возвращает версию, которая возвращает промисы.
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 (кроме ошибки) в массив:
// Оригинальная функция
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);
});
Преобразование методов объекта
При работе с методами объекта необходимо привязать правильный контекст:
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‑ов
// Оригинальный стиль с вложенными 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 для лучшей производительности:
// Преобразовать все методы 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);
}
}
Смешанные последовательные и параллельные операции
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 по‑разному:
// Пользовательский 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 версии нескольких методов:
// Преобразовать весь объект 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:
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‑ов в промисы:
// Хорошая обработка ошибок
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('Файл не найден');
}
}
}
Управление памятью
Будьте осторожны с слушателями событий и очисткой:
function eventPromise(element, event) {
return new Promise((resolve) => {
const handler = () => {
element.removeEventListener(event, handler);
resolve();
};
element.addEventListener(event, handler);
});
}
Показатели производительности
- Повторно используйте promisified функции для лучшей производительности
- Используйте
Promise.allдля независимых операций - Избегайте ненужных цепочек промисов, когда достаточно простых промисов
// Хорошо: повторное использование promisified функций
const readFile = util.promisify(fs.readFile);
// Лучше: использовать Promise.all для чтения нескольких файлов
async function readMultipleFiles(files) {
return Promise.all(files.map(file => readFile(file, 'utf8')));
}
Привязка контекста для методов объекта
При promisification методов объекта убедитесь в правильной привязке контекста:
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
Практические рекомендации
- Начинайте с
util.promisify()для API Node.js — это самый эффективный метод - Для браузерных API сначала проверьте наличие промис‑альтернатив
- Создавайте переиспользуемые promisified функции, а не преобразовывайте «на лету»
- Используйте синтаксис async/await для более чистого и читаемого кода
- Всегда реализуйте надёжную обработку ошибок в цепочках промисов
Связанные вопросы и ответы
- Как обрабатывать ошибки в promisified функциях? Используйте блоки try‑catch с async/await или
.catch()в цепочках промисов - Можно ли преобразовать любой callback в промис? Да, но некоторые сложные случаи могут потребовать пользовательских обёрток
- Каков влияние на производительность от promisification? Минимальное для большинства случаев, но переиспользуйте promisified функции, когда это возможно
Следуя этим техникам, вы сможете эффективно работать с существующими callback‑API, используя современный подход на основе промисов, делая ваш код более поддерживаемым и читаемым, при этом полностью используя возможности асинхронной экосистемы JavaScript.