Понимание асинхронного программирования
Node Hero: Глава 3
Перевод книги Node Hero от RisingStack. Переведено с разрешения правообладателей.
В этой главе я расскажу вам о принципах асинхронного программирования и покажу, как создавать асинхронные операции в JavaScript и Node.js.
Синхронное программирование
В традиционной практике программирования большинство операций ввода-вывода происходит синхронно. Если вы задумайтесь о Java и о том, как вы читаете файл в ней, вы получите что-то вроде этого:
try(FileInputStream inputStream = new FileInputStream("foo.txt")) {
Session IOUtils;
String fileContent = IOUtils.toString(inputStream);
}
Что происходит в фоновом режиме? Основной поток будет заблокирован до тех пор, пока файл не будет прочитан, а это означает, что за это время ничего другого не может быть сделано. Чтобы решить эту проблему и лучше использовать ваш CPU, вам придется управлять потоками вручную.
Если у вас больше блокирующих операций, очередь событий становится ещё хуже:
Для решения этой проблемы Node.js предлагает модель асинхронного программирования.
Асинхронное программирование в Node.js
Асинхронный ввод-вывод — это форма обработки ввода/вывода, позволяющая продолжить обработку других задач, не ожидая завершения передачи.
В следующем примере я покажу простой процесс чтения файлов в Node.js, как синхронным, так и асинхронным способом, с целью показать вам, чего вы можете достигнуть, если будете избегать блокировки ваших приложений.
Начнем с простого примера: синхронное чтение файла с использованием Node.js:
const fs = require('fs')
let content
try {
content = fs.readFileSync('file.md', 'utf-8')
} catch (ex) {
console.log(ex)
}
console.log(content)
Что здесь происходит? Мы читаем файл, используя синхронный интерфейс модуля fs
. Он работает ожидаемым образом: в переменную content
сохраняется содержимое file.md
. Проблема с этим подходом заключается в том, что Node.js будет заблокирована до завершения операции, то есть, пока читается файл, она не может сделать ничего полезного.
Посмотрим, как мы можем это исправить!
Асинхронное программирование, в том виде, в каком мы знаем его в JavaScript, может быть реализовано только при условии, что функции являются объектами первого класса: они могут передаваться как любые другие переменные другим функциям. Функции, которые могут принимать другие функции в качестве аргументов, называются функциями высшего порядка.
Один из самых простых примеров функций высшего порядка:
const numbers = [2,4,1,5,4]
function isBiggerThanTwo (num) {
return num > 2
}
numbers.filter(isBiggerThanTwo)
В приведенном выше примере мы передаем функцию isBiggerThanTwo
в функцию filter
. Таким образом, мы можем определить логику фильтрации.
Так появились функции обратного вызова (колбеки): если вы передаете функцию другой функции в качестве параметра, вы можете вызвать её внутри функции, когда она закончит свою работу. Нет необходимости возвращать значения, нужно только вызывать другую функцию с этими значениями.
В основе Node.js лежит принцип «первым аргументом в колбеке должна быть ошибка». Его придерживаются базовые модули, а также большинство модулей, найденных в NPM.
const fs = require('fs')
fs.readFile('file.md', 'utf-8', function (err, content) {
if (err) {
return console.log(err)
} console.log(content)
})
Что следует здесь выделить:
- обработка ошибок: вместо блока
try-catch
вы проверяете ошибку в колбеке - отсутствует возвращаемое значение: асинхронные функции не возвращают значения, но значения будут переданы в колбеки
Давайте немного изменим этот файл, чтобы увидеть, как это работает на практике:
const fs = require('fs')console.log('start reading a file...')fs.readFile('file.md', 'utf-8', function (err, content) {
if (err) {
console.log('error happened during reading the file')
return console.log(err)
}
console.log(content)
})console.log('end of the file')
Результатом выполнения этого кода будет:
start reading a file...
end of the file
error happened during reading the file
Как вы можете видеть, как только мы начали читать наш файл, выполнение кода продолжилось, а приложение вывело end of the file
. Наш колбек вызвался только после завершения чтения файла. Как такое возможно? Встречайте цикл событий (event loop).
Цикл событий
Цикл событий лежит в основе Node.js и JavaScript и отвечает за планирование асинхронных операций.
Прежде чем погрузиться глубже, давайте убедимся, что мы понимаем, что такое программирование с управлением по событиям (event-driven programming).
Программирование с управлением по событиям представляет собой парадигму программирования, в которой поток выполнения программы определяется событиями, такими как действия пользователя (щелчки мышью, нажатия клавиш), выходы датчиков или сообщения из других программ/потоков.
На практике это означает, что приложения реагируют на события.
Кроме того, как мы уже узнали в первой главе, с точки зрения разработчика Node.js является однопоточным. Это означает, что вам не нужно иметь дело с потоками и синхронизировать их, Node.js скрывает эту сложность за абстракцией. Всё, кроме кода, выполняется параллельно.
Для более глубокого понимания работы цикла событий рекомендуем посмотреть это видео:
Асинхронный поток управления
Поскольку теперь у вас есть общее представление о том, как работает асинхронное программирование в JavaScript, давайте рассмотрим несколько примеров того, как вы можете организовать свой код.
Async.js
Чтобы избежать так называемого Callback-Hell, вы можете начать использовать async.js.
Async.js помогает структурировать ваши приложения и упрощает понимание потока управления.
Давайте рассмотрим короткий пример использования Async.js, а затем перепишем его с помощью промисов.
Следующий фрагмент перебирает три файла и выводит системную информацию по каждому:
async.parallel(['file1', 'file2', 'file3'],
fs.stat,
function (err, results) {
// results теперь содержит массив системных данных для каждого файла
})
Примечание переводчика: если вы пользуетесь Node.js версии 7 и выше, лучше воспользоваться встроенными конструкциями языка, такими как async/await.
Промисы
Объект
Promise
используется для отложенных и асинхронных вычислений. Промис представляет собой операцию, которая еще не завершена, но ожидается в будущем.
На практике предыдущий пример можно переписать следующим образом:
function stats (file) {
return new Promise((resolve, reject) => {
fs.stat(file, (err, data) => {
if (err) {
return reject(err)
}
resolve(data)
})
})
}Promise.all([
stats('file1'),
stats('file2'),
stats('file3')
])
.then((data) => console.log(data))
.catch((err) => console.log(err))
Конечно, если вы используете метод, возвращающий промис, то пример будет заметно компактнее.
В следующей главе вы узнаете, как запустить ваш первый Node.js HTTP-сервер.
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.