Промисы на практике оказались удобнее колбэков из-за того, что не требуют постоянного увеличения вложенности при работе с цепочками асинхронных функций. Более того, на базе промисов строится работа async function.
Все асинхронные функции node.js и подавляющее большинство асинхронных функций внешних модулей на данный момент всё же используют колбэки, чтобы вернуть результат работы или сообщить об ошибке.
Технически, не сложно обернуть асинхронную функцию в промиз. Но, к сожалению, это не одна строчка кода.
const fs = require('fs');
function reader(path) {
return new Promise((resolve, reject) => {
fs.readdir(path, (err, list) => {
if (err) {
reject(err);
return;
}
resolve(list);
});
});
}
reader('.')
.then(list => {
console.log(list);
process.exit(0);
})
.catch(err => {
console.error(err);
process.exit(1);
});
В стандартной библиотеке node.js 8 util
появилась функция promisify(), которая как раз решает эту задачу.
const fs = require('fs');
const util = require('util');
const reader = util.promisify(fs.readdir);
reader('.')
.then(list => {
console.log(list);
process.exit(0);
})
.catch(err => {
console.error(err);
process.exit(1);
});
Функция promisify()
придерживается соглашения:
- колбек в параметрах исходной функции всегда идет последним;
- первым параметром колбека приходит объект ошибки, если она есть.
Экспериментируя с этим новым методом я почти сразу натолкнулся на ситуацию, когда мой код переставал работать ожидаемым образом.
function Timer(delay) {
this._delay = delay;
}
Timer.prototype.start = function (callback) {
setTimeout(callback, this._delay);
};
const timer = new Timer(1000);
timer.start(() => {
console.log('Finish');
});
Из метода start()
я хочу сделать новую функцию, возвращающую мне промиз.
const start = util.promisify(timer.start);
start().then(() => {
console.log('Finish');
});
Но этот фрагмент кода перестаёт работать. Он начинает выдавать ошибку:
TypeError: Cannot read property '_delay' of undefined
Проблема кроется в том, что метод внутри себя использует ссылку на экземпляр класса Timer
— в частности, он вытаскиваем величину задержки. А так как метод был вызван вне контекста экземпляра таймера, то this
оказалось не определено.
Если вернуться к примеру с колбэком, то следующий фрагмент кода тоже не будет работать и выдаст такую же ошибку.
const start = timer.start;
start(() => {
console.log('Finish');
});
Исправить ситуацию можно несколькими путями.
Связать функцию с экземпляром
const start = util.promisify(timer.start).bind(timer);
Теперь не важно как мы вызовем функцию
start()
. Она всегда будет иметь экземпляр таймера в качестве контекста исполнения.
Сохранить функцию в экземпляр класса
timer.start = util.promisify(timer.start);
timer.start().then(() => {
console.log('Finish');
});
Для данного экземпляра мы подменили реализацию метода из прототипа на новую функцию. Контекст исполнения будет передаваться естественным образом.
Развивая этот вариант дальше, новый метод можно сохранить в прототипе класса.
Timer.prototype.start = util.promisify(Timer.prototype.start);
Такой вариант опасен тем, что логика работы изменится для абсолютно всех экземпляров данного класса. Возможно, это не страшно для кода, который вы сами пишете и обслуживаете. Но, вряд-ли будет приемлемо для сторонних модулей.