Преобразование обратных вызовов в обещания в Node.js

Введение Несколько лет назад обратные вызовы были единственным способом добиться асинхронного выполнения кода в JavaScript. Проблем с обратными вызовами было немного, и самой заметной из них был «Ад обратного вызова» [/ избегая-callback-hell-in-node-js /]. В ES6 Promises были введены как решение этих проблем. И, наконец, были введены ключевые слова async / await для еще более приятного восприятия и улучшения читаемости. Даже с добавлением новых подходов остается еще много

Вступление

Несколько лет назад обратные вызовы были единственным способом добиться асинхронного выполнения кода в JavaScript. Проблем с обратными вызовами было немного, и самая заметная из них - "Ад обратных вызовов".

В ES6 Promises были введены как решение этих проблем. И, наконец, async/await для еще более приятного восприятия и улучшения читаемости.

Даже с добавлением новых подходов все еще существует множество собственных модулей и библиотек, использующих обратные вызовы. В этой статье мы поговорим о том, как преобразовать обратные вызовы JavaScript в обещания. Знание ES6 пригодится, поскольку мы будем использовать такие функции, как операторы распространения, чтобы упростить задачу.

Что такое обратный звонок

Обратный вызов - это аргумент функции, который является самой функцией. Хотя мы можем создать любую функцию для приема другой функции, обратные вызовы в основном используются в асинхронных операциях.

JavaScript - это интерпретируемый язык, который может обрабатывать только одну строку кода за раз. Некоторые задачи могут занять много времени, например загрузка или чтение большого файла. JavaScript переносит эти длительные задачи в другой процесс в браузере или в среде Node.js. Таким образом, он не блокирует выполнение всего остального кода.

Обычно асинхронные функции принимают функцию обратного вызова, поэтому, когда они будут завершены, мы сможем обработать их данные.

Давайте возьмем пример, мы напишем функцию обратного вызова, которая будет выполняться, когда программа успешно прочитает файл с нашего жесткого диска.

Для этого мы будем использовать текстовый файл с именем sample.txt , содержащий следующее:

 Hello world from sample.txt 

Затем напишем простой скрипт Node.js для чтения файла :

 const fs = require('fs'); 
 
 fs.readFile('./sample.txt', 'utf-8', (err, data) => { 
 if (err) { 
 // Handle error 
 console.error(err); 
 return; 
 } 
 
 // Data is string do something with it 
 console.log(data); 
 }); 
 
 for (let i = 0; i < 10; i++) { 
 console.log(i); 
 } 

Запуск этого кода должен дать:

 0 
 ... 
 8 
 9 
 Hello world from sample.txt 

Если вы запустите этот код, вы должны увидеть, что 0..9 печатается перед выполнением обратного вызова. Это связано с асинхронным управлением JavaScript, о котором мы говорили ранее. Обратный вызов, который регистрирует содержимое файла, будет вызываться только после чтения файла.

В качестве примечания, обратные вызовы также могут использоваться в синхронных методах. Например, Array.sort() принимает функцию обратного вызова, которая позволяет вам настроить сортировку элементов.

Функции, которые принимают обратные вызовы, называются функциями высшего порядка .

Теперь у нас есть лучшее представление об обратных вызовах. Давайте продолжим и посмотрим, что такое Promise.

Что такое обещание

Обещания были введены в ECMAScript 2015 (широко известном как ES6 ), чтобы улучшить взаимодействие разработчиков с асинхронным программированием. Как следует из названия, это обещание, что объект JavaScript в конечном итоге вернет значение или ошибку .

Обещание имеет 3 состояния:

  • Ожидание : начальное состояние, указывающее, что асинхронная операция не завершена.
  • Выполнено : означает, что асинхронная операция завершилась успешно.
  • Отклонено : это означает, что асинхронная операция завершилась неудачно.

Большинство обещаний в конечном итоге выглядят так:

 someAsynchronousFunction() 
 .then(data => { 
 // After promise is fulfilled 
 console.log(data); 
 }) 
 .catch(err => { 
 // If promise is rejected 
 console.error(err); 
 }); 

Обещания важны в современном JavaScript, поскольку они используются с async/await которые были введены в ECMAScript 2016 . С async/await нам не нужно использовать обратные вызовы или then() и catch() для написания асинхронного кода.

Если бы предыдущий пример был адаптирован, это выглядело бы так:

 try { 
 const data = await someAsynchronousFunction(); 
 } catch(err) { 
 // If promise is rejected 
 console.error(err); 
 } 

Это очень похоже на «обычный» синхронный JavaScript! Вы можете узнать больше об async/await в нашей статье Node.js Async Await в ES7 .

Самые популярные библиотеки JavaScript и новые проекты используют обещания с ключевыми словами async/await

Однако, если вы обновляете существующее репо или сталкиваетесь с устаревшей базой кода, вам, вероятно, будет интересно перенести API на основе обратного вызова на API на основе Promise, чтобы улучшить опыт разработки. Ваша команда тоже будет вам благодарна.

Давайте рассмотрим несколько методов преобразования обратных вызовов в обещания!

Преобразование обратного вызова в обещание

Node.js Promisify

Большинство асинхронных функций, которые принимают обратный вызов в Node.js, такие как fs (файловая система), имеют стандартный стиль реализации - обратный вызов передается в качестве последнего параметра.

Например, вот как вы можете прочитать файл с помощью fs.readFile() без указания кодировки текста:

 fs.readFile('./sample.txt', (err, data) => { 
 if (err) { 
 console.error(err); 
 return; 
 } 
 
 // Data is a buffer 
 console.log(data); 
 }); 

Примечание . Если utf-8 вы получите строковый вывод. Если вы не укажете кодировку, вы получите вывод Buffer

Более того, обратный вызов, который передается функции, должен принимать Error как первый параметр. После этого выходов может быть любое количество.

Если функция, которую вам нужно скрыть в Promise, следует этим правилам, вы можете использовать util.promisify , собственный модуль Node.js, который выполняет обратные вызовы для Promises.

Для этого сначала импортируйте модуль util

 const util = require('util'); 

Затем вы используете promisify чтобы скрыть его до обещания:

 const fs = require('fs'); 
 const readFile = util.promisify(fs.readFile); 

Теперь используйте вновь созданную функцию как обычное обещание:

 readFile('./sample.txt', 'utf-8') 
 .then(data => { 
 console.log(data); 
 }) 
 .catch(err => { 
 console.log(err); 
 }); 

В качестве альтернативы вы можете использовать async/await как показано в следующем примере:

 const fs = require('fs'); 
 const util = require('util'); 
 
 const readFile = util.promisify(fs.readFile); 
 
 (async () => { 
 try { 
 const content = await readFile('./sample.txt', 'utf-8'); 
 console.log(content); 
 } catch (err) { 
 console.error(err); 
 } 
 })(); 

Вы можете использовать await внутри функции, которая была создана с помощью async , поэтому в этом примере у нас есть оболочка функции. Эта функция-оболочка также известна как выражение немедленного вызова функции .

Если ваш обратный вызов не соответствует этому конкретному стандарту, не волнуйтесь. Функция util.promisify() позволяет настроить способ преобразования.

Примечание . Обещания стали популярными вскоре после их появления. Node.js уже преобразовал большинство, если не все, свои основные функции из обратного вызова в API на основе Promise.

Если вам нужно работать с файлами с помощью обещаний, воспользуйтесь библиотекой, поставляемой с Node.js.

До сих пор вы узнали, как скрыть обратные вызовы стандартного стиля Node.js в обещания. Этот модуль доступен только в Node.js начиная с версии 8. Если вы работаете в браузере или в более ранней версии Node, вам, вероятно, будет лучше создать свою собственную версию функции на основе обещаний.

Создание вашего обещания

Давайте поговорим о том, как util.promisify() недоступна.

Идея состоит в том, чтобы создать новый Promise который обтекает функцию обратного вызова. Если функция обратного вызова возвращает ошибку, мы отклоняем обещание с ошибкой. Если функция обратного вызова возвращает вывод без ошибок, мы разрешаем Promise с выводом.

Начнем с преобразования обратного вызова в обещание для функции, которая принимает фиксированное количество параметров:

 const fs = require('fs'); 
 
 const readFile = (fileName, encoding) => { 
 return new Promise((resolve, reject) => { 
 fs.readFile(fileName, encoding, (err, data) => { 
 if (err) { 
 return reject(err); 
 } 
 
 resolve(data); 
 }); 
 }); 
 } 
 
 readFile('./sample.txt') 
 .then(data => { 
 console.log(data); 
 }) 
 .catch(err => { 
 console.log(err); 
 }); 

Наша новая функция readFile() принимает два аргумента, которые мы использовали для чтения файлов с помощью fs.readFile() . Затем мы создаем новый Promise , который охватывает функцию, которая принимает обратный вызов, в данном случае fs.readFile() .

Вместо того, чтобы возвращать ошибку, мы reject обещание. Вместо того, чтобы немедленно регистрировать данные, мы resolve Promise. Затем мы, как и раньше, используем нашу основанную на readFile()

Давайте попробуем другую функцию, которая принимает динамическое количество параметров:

 const getMaxCustom = (callback, ...args) => { 
 let max = -Infinity; 
 
 for (let i of args) { 
 if (i > max) { 
 max = i; 
 } 
 } 
 
 callback(max); 
 } 
 
 getMaxCustom((max) => { console.log('Max is ' + max) }, 10, 2, 23, 1, 111, 20); 

Параметр обратного вызова также является первым параметром, что немного необычно для функций, которые принимают обратные вызовы.

Преобразование в обещание выполняется таким же образом. Мы создаем новый Promise который обтекает нашу функцию, использующую обратный вызов. Затем мы reject , если возникает ошибка и resolve , когда мы имеем результат.

Наша обещанная версия выглядит так:

 const getMaxPromise = (...args) => { 
 return new Promise((resolve) => { 
 getMaxCustom((max) => { 
 resolve(max); 
 }, ...args); 
 }); 
 } 
 
 getMaxCustom(10, 2, 23, 1, 111, 20) 
 .then(max => console.log(max)); 

При создании нашего обещания не имеет значения, использует ли функция обратные вызовы нестандартным образом или с большим количеством аргументов. Мы полностью контролируем, как это делается, и принципы одинаковы.

Заключение

В то время как обратные вызовы были способом по умолчанию использовать асинхронный код в JavaScript, Promises - более современный метод, который, по мнению разработчиков, проще в использовании. Если мы когда-нибудь встретим кодовую базу, которая использует обратные вызовы, теперь мы можем сделать эту функцию обещанием.

В этой статье вы впервые увидели, как использовать utils.promisfy() в Node.js для преобразования функций, принимающих обратные вызовы, в обещания. Затем вы увидели, как создать собственный Promise который обтекает функцию, которая принимает обратный вызов, без использования внешних библиотек.

Благодаря этому большой объем устаревшего кода JavaScript можно легко смешать с более современными кодовыми базами и практиками! Как всегда, исходный код доступен на GitHub .

comments powered by Disqus