Як написати власну функцію Promisify з нуля

Вступ

У цій статті ви дізнаєтеся, як написати власну функцію promisify з нуля.

Promisification допомагає мати справу з API зворотного виклику, зберігаючи при цьому код узгодженим із обіцянками.

Ми могли б просто обернути будь-яку функцію new Promise()і взагалі не хвилюватися про це. Але робити це, коли у нас багато функцій, було б зайвим.

Якщо ви розумієте обіцянки та зворотні дзвінки, то навчитися писати функції promisify має бути легко. Тож давайте почнемо.

Але чи задумувались ви коли-небудь, як працює promisify?

Найголовніше - не припиняти допити. Допитливість має свою причину існування.

- Альберт Ейнштейн

Обіцянки були введені у стандарт ECMA-262, 6-е видання (ES6), який було опубліковано в червні 2015 року.

Це було значним покращенням у порівнянні із зворотними викликами, оскільки всі ми знаємо, наскільки нечитабельним може бути "пекло зворотного дзвінка" :)

Як розробник Node.js, ви повинні знати, що таке обіцянка і як це працює внутрішньо, що також допоможе вам в інтерв’ю JS. Не соромтеся швидко переглядати їх, перш ніж читати далі.

Чому нам потрібно перетворювати зворотні дзвінки на обіцянки?

  1. Для зворотних викликів, якщо ви хочете зробити щось послідовно, вам доведеться вказати errаргумент у кожному зворотному виклику, який є зайвим. У promises або async-await ви можете просто додати .catchметод або блок, який буде виявляти будь-які помилки, що виникли в ланцюжку обіцянок
  2. Завдяки зворотним викликам ви не можете контролювати, коли його викликають, в якому контексті чи скільки разів він викликається, що може призвести до витоків пам'яті.
  3. Використовуючи обіцянки, ми контролюємо ці фактори (особливо обробку помилок), завдяки чому код стає більш читабельним і ремонтопридатним.

Як змусити функції на основі зворотного дзвінка повертати обіцянку

Є два способи зробити це:

  1. Оберніть функцію в іншу функцію, яка повертає обіцянку. Потім він вирішує або відхиляє на основі аргументів зворотного виклику.
  2. Промісифікація - Ми створюємо утиліту / допоміжну функцію, 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.