Как да напишете собствената си функция Promisify от нулата

Въведение

В тази статия ще научите как да пишете собствената си функция за обещаване от нулата.

Promisification помага при справяне с API-та, базирани на обратно повикване, като същевременно поддържа кода в съответствие с обещанията.

Можем просто да обгърнем всяка функция new Promise()и изобщо да не се тревожим за нея. Но това, когато имаме много функции, би било излишно.

Ако разбирате обещания и обратно извикване, тогава научаването как да пишете обещаващи функции трябва да е лесно. Така че нека да започнем.

Но замисляли ли сте се как работи promisify?

Важното е да не спирате да разпитвате. Любопитството има своя причина за съществуване.

- Алберт Айнщайн

Обещанията бяха въведени в стандарта ECMA-262, 6-то издание (ES6), публикувано през юни 2015 г.

Това беше доста подобрение спрямо обратните обаждания, тъй като всички знаем колко нечетлив може да бъде „адът на обратно обаждане“ :)

Като разработчик на Node.js трябва да знаете какво е обещание и как работи вътрешно, което също ще ви помогне в JS интервюта. Чувствайте се свободни да ги прегледате бързо, преди да прочетете.

Защо трябва да конвертираме обратно извикване в обещания?

  1. С обратните обаждания, ако искате да направите нещо последователно, ще трябва да посочите errаргумент във всяко обратно извикване, което е излишно. В обещания или async-await можете просто да добавите .catchметод или блок, който да улови всички грешки, възникнали във веригата за обещания
  2. С обратните обаждания нямате контрол кога е извикан, под какъв контекст или колко пъти е извикан, което може да доведе до изтичане на памет.
  3. Използвайки обещания, ние контролираме тези фактори (особено обработката на грешки), така че кодът е по-четлив и поддържаем.

Как да накарам функциите, базирани на обратно извикване, да връщат обещание

Има два начина да го направите:

  1. Опаковайте функцията в друга функция, която връща обещание. След това разрешава или отхвърля въз основа на аргументи за обратно извикване.
  2. Promisification - Ние създаваме util / helper функция, 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

Когато трябва да обещаем много функции, можем да създадем помощна функция 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). Тогава нашата обещаваща функция трябва да бъде в състояние да 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.