Как избежать ада обратного вызова в Node.js

Введение Признаюсь, что я был одним из тех людей, которые решили изучить Node.js просто из-за шума вокруг него и того, как много о нем говорили. Я подумал, что в этом должно быть что-то особенное, если он получил такую поддержку на столь раннем этапе своей жизни. В основном я работал с C, Java и Python, поэтому асинхронный стиль JavaScript сильно отличался от всего, с чем я сталкивался раньше. Как многие из вас, вероятно, знают, весь JavaScript на самом деле является однопоточным

Вступление

Признаюсь, я был одним из тех людей, которые решили изучить Node.js просто из-за шума вокруг него и того, как много о нем говорили. Я подумал, что в этом должно быть что-то особенное, если он получил такую поддержку на столь раннем этапе своей жизни. В основном я работал с C, Java и Python, поэтому асинхронный стиль JavaScript сильно отличался от всего, с чем я сталкивался раньше.

Как многие из вас, вероятно, знают, весь JavaScript на самом деле представляет собой однопоточный цикл обработки событий, который обрабатывает события в очереди. Если бы вы выполняли длительную задачу в одном потоке, то процесс блокировался бы, в результате чего другим событиям приходилось ждать обработки (например, пользовательский интерфейс зависает, данные не сохраняются и т. Д.). Это именно то, чего вы хотите избежать в системе, управляемой событиями. Вот отличное видео, объясняющее гораздо больше о цикле событий JavaScript.

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

 downloadFile('example.com/weather.json', function(err, data) { 
 console.log('Got weather data:', data); 
 }); 

Проблема: ад обратного вызова

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

 getData(function(a){ 
 getMoreData(a, function(b){ 
 getMoreData(b, function(c){ 
 getMoreData(c, function(d){ 
 getMoreData(d, function(e){ 
 ... 
 }); 
 }); 
 }); 
 }); 
 }); 

Как видите, это действительно может выйти из-под контроля. Добавьте несколько операторов if for циклов, вызовов функций или комментариев, и вы получите очень трудный для чтения код. Жертвами этого особенно становятся новички, не понимающие, как избежать этой «пирамиды гибели».

Альтернативы

Дизайн вокруг этого

Так много программистов попадают в ад обратных вызовов только из-за этого (плохого дизайна). На самом деле они не думают о структуре своего кода заранее и не осознают, насколько плохой стал их код, пока не становится слишком поздно. Как и в случае с любым другим кодом, который вы пишете, вам следует остановиться и подумать о том, что можно сделать, чтобы сделать его более простым и читабельным до или во время написания. Вот несколько советов, которые вы можете использовать, чтобы избежать ада обратных вызовов (или, по крайней мере, справиться с этим).

Использовать модули

Практически в каждом языке программирования одним из лучших способов снизить сложность является модульное построение. Программирование на JavaScript ничем не отличается. Каждый раз, когда вы пишете код, найдите время, чтобы сделать шаг назад и выяснить, существует ли общий шаблон, с которым вы часто сталкиваетесь.

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

Существуют тысячи модулей, на которые вы можете ссылаться, но вот некоторые из них, которые стоит рассмотреть. Они обрабатывают общие, но очень специфические задачи, которые в противном случае загромождают ваш код и ухудшают читаемость: Pluralize, csv , qs , clone .

Дайте вашим функциям имена

При чтении кода (особенно беспорядочного, неорганизованного кода) легко потерять логический поток или даже синтаксис, когда небольшие пространства перегружены таким количеством вложенных обратных вызовов. Один из способов помочь в борьбе с этим - назвать свои функции, поэтому все, что вам нужно сделать, это взглянуть на имя, и вы лучше поймете, что оно делает. Это также дает вашим глазам ориентир по синтаксису.

Рассмотрим следующий код:

 var fs = require('fs'); 
 
 var myFile = '/tmp/test'; 
 fs.readFile(myFile, 'utf8', function(err, txt) { 
 if (err) return console.log(err); 
 
 txt = txt + '\nAppended something!'; 
 fs.writeFile(myFile, txt, function(err) { 
 if(err) return console.log(err); 
 console.log('Appended text!'); 
 }); 
 }); 

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

 var fs = require('fs'); 
 
 var myFile = '/tmp/test'; 
 fs.readFile(myFile, 'utf8', function appendText(err, txt) { 
 if (err) return console.log(err); 
 
 txt = txt + '\nAppended something!'; 
 fs.writeFile(myFile, txt, function notifyUser(err) { 
 if(err) return console.log(err); 
 console.log('Appended text!'); 
 }); 
 }); 

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

Объявите свои функции заранее

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

Итак, вы могли уйти от этого ...

 var fs = require('fs'); 
 
 var myFile = '/tmp/test'; 
 fs.readFile(myFile, 'utf8', function(err, txt) { 
 if (err) return console.log(err); 
 
 txt = txt + '\nAppended something!'; 
 fs.writeFile(myFile, txt, function(err) { 
 if(err) return console.log(err); 
 console.log('Appended text!'); 
 }); 
 }); 

...к этому:

 var fs = require('fs'); 
 
 function notifyUser(err) { 
 if(err) return console.log(err); 
 console.log('Appended text!'); 
 }; 
 
 function appendText(err, txt) { 
 if (err) return console.log(err); 
 
 txt = txt + '\nAppended something!'; 
 fs.writeFile(myFile, txt, notifyUser); 
 } 
 
 var myFile = '/tmp/test'; 
 fs.readFile(myFile, 'utf8', appendText); 

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

Async.js

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

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

Async, как и все остальное, не идеален. Очень легко увлечься объединением последовательностей, параллелей, вечности и т. Д., И в этот момент вы вернетесь к тому месту, где вы начали с беспорядочного кода. Будьте осторожны, чтобы не оптимизировать преждевременно. То, что несколько асинхронных задач могут выполняться параллельно, не всегда означает, что они должны это делать. В действительности, поскольку Node является только однопоточным, параллельное выполнение задач с использованием Async практически не дает прироста производительности.

Приведенный выше код можно упростить с помощью водопада Async:

 var fs = require('fs'); 
 var async = require('async'); 
 
 var myFile = '/tmp/test'; 
 
 async.waterfall([ 
 function(callback) { 
 fs.readFile(myFile, 'utf8', callback); 
 }, 
 function(txt, callback) { 
 txt = txt + '\nAppended something!'; 
 fs.writeFile(myFile, txt, callback); 
 } 
 ], function (err, result) { 
 if(err) return console.log(err); 
 console.log('Appended text!'); 
 }); 

Обещания

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

Вот пример, использующий очень быструю и очень популярную библиотеку Promise Bluebird :

 var Promise = require('bluebird'); 
 var fs = require('fs'); 
 Promise.promisifyAll(fs); 
 
 var myFile = '/tmp/test'; 
 fs.readFileAsync(myFile, 'utf8').then(function(txt) { 
 txt = txt + '\nAppended something!'; 
 fs.writeFile(myFile, txt); 
 }).then(function() { 
 console.log('Appended text!'); 
 }).catch(function(err) { 
 console.log(err); 
 }); 

Обратите внимание, что это решение не только короче, чем предыдущие, но и его легче читать (хотя, надо признать, к коду в стиле Promise нужно привыкнуть). Найдите время, чтобы изучить и понять обещания, это того стоит. Однако обещания определенно не являются решением всех наших проблем в асинхронном программировании, поэтому не предполагайте, что, используя их, вы получите быстрое, чистое приложение без ошибок. Ключ в том, чтобы знать, когда они будут вам полезны.

Вы должны проверить несколько библиотек Promise: Q , Bluebird или встроенные Promises, если вы используете ES6.

Асинхронный / Ожидание

Примечание. Это функция ES7, которая в настоящее время не поддерживается ни в Node, ни в io.js. Однако вы можете использовать его прямо сейчас с транспилером, таким как Babel .

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

Пример:

 async function getUser(id) { 
 if (id) { 
 return await db.user.byId(id); 
 } else { 
 throw 'Invalid ID!'; 
 } 
 } 
 
 try { 
 let user = await getUser(123); 
 } catch(err) { 
 console.error(err); 
 } 

db.user.byId(id) возвращает Promise , который мы обычно должны использовать с .then() , но с await мы можем напрямую вернуть разрешенное значение.

Обратите внимание, что функция, содержащая await имеет префикс async , который сообщает нам, что она содержит асинхронный код и также должна вызываться с помощью await .

Еще одним большим преимуществом этого метода является то, что теперь мы можем использовать try/catch , for и while с нашими асинхронными функциями, что гораздо более интуитивно понятно, чем объединение обещаний в цепочку.

Помимо использования транспилеров, таких как Babel и Traceur , вы также можете получить подобную функциональность в Node с пакетом asyncawait.

Заключение

Избежать таких распространенных проблем, как ад обратного вызова, непросто, поэтому не ожидайте, что ваши разочарования исчезнут сразу. Мы все попадаем в ловушку этого. Просто попробуйте сбавить скорость и подумайте о структуре вашего кода. Как и все остальное, практика ведет к совершенству.

Вы сбежали в ад обратных звонков? Если да, то как это обойти? Сообщите нам в комментариях!

comments powered by Disqus