Вступ
У цій статті ви дізнаєтеся, як написати власну функцію promisify з нуля.
Promisification допомагає мати справу з API зворотного виклику, зберігаючи при цьому код узгодженим із обіцянками.
Ми могли б просто обернути будь-яку функцію new Promise()
і взагалі не хвилюватися про це. Але робити це, коли у нас багато функцій, було б зайвим.
Якщо ви розумієте обіцянки та зворотні дзвінки, то навчитися писати функції promisify має бути легко. Тож давайте почнемо.
Але чи задумувались ви коли-небудь, як працює promisify?
Найголовніше - не припиняти допити. Допитливість має свою причину існування.- Альберт Ейнштейн
Обіцянки були введені у стандарт ECMA-262, 6-е видання (ES6), який було опубліковано в червні 2015 року.
Це було значним покращенням у порівнянні із зворотними викликами, оскільки всі ми знаємо, наскільки нечитабельним може бути "пекло зворотного дзвінка" :)

Як розробник Node.js, ви повинні знати, що таке обіцянка і як це працює внутрішньо, що також допоможе вам в інтерв’ю JS. Не соромтеся швидко переглядати їх, перш ніж читати далі.
Чому нам потрібно перетворювати зворотні дзвінки на обіцянки?
- Для зворотних викликів, якщо ви хочете зробити щось послідовно, вам доведеться вказати
err
аргумент у кожному зворотному виклику, який є зайвим. У promises або async-await ви можете просто додати.catch
метод або блок, який буде виявляти будь-які помилки, що виникли в ланцюжку обіцянок - Завдяки зворотним викликам ви не можете контролювати, коли його викликають, в якому контексті чи скільки разів він викликається, що може призвести до витоків пам'яті.
- Використовуючи обіцянки, ми контролюємо ці фактори (особливо обробку помилок), завдяки чому код стає більш читабельним і ремонтопридатним.
Як змусити функції на основі зворотного дзвінка повертати обіцянку
Є два способи зробити це:
- Оберніть функцію в іншу функцію, яка повертає обіцянку. Потім він вирішує або відхиляє на основі аргументів зворотного виклику.
- Промісифікація - Ми створюємо утиліту / допоміжну функцію,
promisify
яка трансформує всі API на основі зворотного виклику помилок.
Приклад: існує API на основі зворотного виклику, який надає суму двох чисел. Ми хочемо обізнати його, щоб він повернув thenable
обіцянку.
const getSumAsync = (num1, num2, callback) => { if (!num1 || !num2) { return callback(new Error("Missing arguments"), null); } return callback(null, num1 + num2); } getSumAsync(1, 1, (err, result) => { if (err){ doSomethingWithError(err) }else { console.log(result) // 2 } })
Загорніть в обіцянку
Як бачите, getSumPromise
делегує всю роботу на вихідну функцію getSumAsync
, надаючи власний зворотний виклик, що перекладається як обіцянка resolve/reject
.
Промісифікувати
Коли нам потрібно оптимізувати багато функцій, ми можемо створити допоміжну функцію promisify
.
Що таке промісифікація?
Промісіфікація означає трансформацію. Це перетворення функції, яка приймає зворотний виклик, у функцію, що повертає обіцянку.
Використання Node.js util.promisify()
:
const { promisify } = require('util') const getSumPromise = promisify(getSumAsync) // step 1 getSumPromise(1, 1) // step 2 .then(result => { console.log(result) }) .catch(err =>{ doSomethingWithError(err); })
Отже, це виглядає як магічна функція, яка перетворюється getSumAsync
в getSumPromise
яку має .then
і .catch
методи
Давайте напишемо власну функцію promisify:
Якщо ви подивитеся на крок 1 у наведеному вище коді, promisify
функція приймає функцію як аргумент, тому перше, що нам потрібно зробити, напишіть функцію, яка може зробити те саме:
const getSumPromise = myPromisify(getSumAsync) const myPromisify = (fn) => {}
Після цього getSumPromise(1, 1)
відбувається виклик функції. Це означає, що наш promisify повинен повернути іншу функцію, яку можна викликати з тими ж аргументами вихідної функції:
const myPromisify = (fn) => { return (...args) => { } }
У наведеному вище коді ви бачите, що ми поширюємо аргументи, оскільки ми не знаємо, скільки аргументів має оригінальна функція. args
буде масивом, що містить усі аргументи.
Коли ти телефонуєш getSumPromise(1, 1)
, ти насправді телефонуєш (...args)=> {}
. У реалізації вище він повертає обіцянку. Ось чому ви можете використовувати getSumPromise(1, 1).then(..).catch(..)
.
Сподіваюся, ви отримали натяк на те, що функція обгортки (...args) => {}
повинна повертати обіцянку.
Поверніть обіцянку
const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { }) } }
Тепер складна частина полягає в тому, як вирішити, коли потрібно resolve or reject
обіцяти.
Власне, це вирішить оригінальна getSumAsync
реалізація функції - вона буде викликати оригінальну функцію зворотного виклику, і нам просто потрібно її визначити. Тоді на основі err
і result
ми будемо reject
або resolve
обіцянку.
const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { function customCallback(err, result) { if (err) { reject(err) }else { resolve(result); } } }) } }
Ми складаємось args[]
лише з аргументів, переданих, getSumPromise(1, 1)
крім функції зворотного виклику. Отже, вам потрібно додати customCallback(err, result)
до того, args[]
який вихідна функція getSumAsync
буде викликати відповідно, оскільки ми відстежуємо результат customCallback
.
Надіслати customCallback до аргументів []
const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { function customCallback(err, result) { if (err) { reject(err) }else { resolve(result); } } args.push(customCallback) fn.call(this, ...args) }) } }
Як бачите, ми додали fn.call(this, args)
, що буде викликати вихідну функцію в тому ж контексті, що і аргументи getSumAsync(1, 1, customCallback)
. Тоді наша функція promisify повинна мати можливість resolve/reject
відповідно.
The above implementation will work when the original function expects a callback with two arguments, (err, result)
. That’s what we encounter most often. Then our custom callback is in exactly the right format and promisify
works great for such a case.
But what if the original fn
expects a callback with more arguments likecallback(err, result1, result2, ...)
?
In order to make it compatible with that, we need to modify our myPromisify
function which will be an advanced version.
const myPromisify = (fn) => { return (...args) => { return new Promise((resolve, reject) => { function customCallback(err, ...results) { if (err) { return reject(err) } return resolve(results.length === 1 ? results[0] : results) } args.push(customCallback) fn.call(this, ...args) }) } }
Example:
const getSumAsync = (num1, num2, callback) => { if (!num1 || !num2) { return callback(new Error("Missing dependencies"), null); } const sum = num1 + num2; const message = `Sum is ${sum}` return callback(null, sum, message); } const getSumPromise = myPromisify(getSumAsync) getSumPromise(2, 3).then(arrayOfResults) // [6, 'Sum is 6']
That’s all! Thank you for making it this far!
I hope you’re able to grasp the concept. Try to re-read it again. It’s a bit of code to wrap your head around, but not too complex. Let me know if it was helpful ?
Don’t forget to share it with your friends who are starting with Node.js or need to level up their Node.js skills.
References:
//nodejs.org/dist/latest-v8.x/docs/api/util.html#util_util_promisify_original
//github.com/digitaldesignlabs/es6-promisify
You can read other articles like this at 101node.io.