Синхронне програмування
У традиційній практиці програмування, більшість операцій вводу/виводу відбуваються синхронно. Наприклад, в Java ми б зчитували файл приблизно так:
try(FileInputStream inputStream = new FileInputStream("foo.txt")) {
Session IOUtils;
String fileContent = IOUtils.toString(inputStream);
}
Основний потік буде заблокований під час зчитування файлу, що означає, що одночасно виконувати інші дії буде неможливо. Для того, щоб вирішити цю проблему, вам доведеться керувати потоками вручну.
Якщо у вас є більше блокуючих операцій, то ситуація погіршується:
(Червоні стовпчики показують, коли процес чекає відповіді від зовнішніх ресурсів і коли його заблоковано, чорні стовпчики показують, коли ваш код працює, зелені стовпчики показують іншу частину додатка)
Щоб вирішити цю проблему Node.js представив модель асинхронного програмування.
Асинхронне програмування в Node.js
Асинхронний ввід/вивід є формою I/O, що дає змогу продовжувати інші операції до того, як певна дія буде завершена.
У наступному прикладі я покажу вам простий процес зчитування файлів в 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 - може бути досягнуто тільки з нативними функціями мови: вони можуть передаватися іншим функціям, як і будь-які інші змінні. Функції, які можуть приймати інші функції в якості аргументів, називаються функціями вищого порядку (higher-order functions).
Один з найпростіших прикладів функцій вищого порядку:
const numbers = [2,4,1,5,4]
function isBiggerThanTwo (num) {
return num > 2
}
numbers.filter(isBiggerThanTwo)
У наведеному вище прикладі ми передаємо функцію функції filter
. Таким чином, ми можемо визначити логіку фільтрації.
Так народилися callback'и: якщо ви передаєте функцію іншій функції в якості параметра, ви можете її викликати з іншою функцією. Немає необхідності повертати значення, тільки виклик іншої функції зі значеннями.
Ці так звані error-first callback'и знаходяться в самому серці самого 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
вам слід перевірити на наявність помилок callback-функцію -
не повертає значень: асинхронні функції не повертають значень, але значення будуть передані callback-функціям
Давайте трохи змінимо цей файл, щоб побачити, як це працює на практиці:
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
. Наша callback-функція була викликана тільки коли зчитування файлу було завершено. Як це можливо? Зустрічайте цикл подій (event loop).
Цикл подій
Цикл подій знаходиться в самому серці Node.js/JavaScript - він відповідає за планування асинхронних операцій.
Перш ніж занурюватися глибше, давайте розберемося, що таке event-driven-програмування.
Event-driven-програмування - це парадигма програмування, в якій потік програми визначається подіями, такими як дії користувача (клацання миші, натискання на клавіатуру), або повідомлень з інших програм/потоків.
На практиці, це означає, що додаток оперує подіями.
Крім того, як ми вже знаємо з першого розділу, Node.js є однопотоковим - з точки зору розробника. Це означає, що вам не доведеться мати справу з потоками і їх синхронізацією, Node.js позбавляє вас від цієї проблеми. Все, крім вашого коду, проводиться паралельно.
Щоб зрозуміти цикл подій глибше, подивіться це відео:
Асинхронне керування потоками
Тепер у вас є загальне розуміння того, як працює асинхронне програмування в JavaScript, давайте розглянемо кілька прикладів, як ви можете організувати свій код.
Async.js
Для того, щоб уникнути так званого Callback-пекла використовуйте async.js.
Async.js допомагає структурувати ваші додатки і робить управління потоками легше.
Наведемо короткий приклад використання Async.js, а потім перепишемо його, використовуючи Promises.
Наступний фрагмент коду проходиться по трьом файлам для статистики:
async.parallel(['file1', 'file2', 'file3'], fs.stat, function (err, results) {
// результат - масив статистики для кожного файлу
})
Promises
Об'єкт Promise використовується для відкладених і асинхронних обчислень. 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))
Звичайно, якщо ви використовуєте метод, який має інтерфейс Promise, то Promise-приклад може бути ще коротше.
Далі: Частина 4 - Ваш перший Node.js HTTP сервер
Ще немає коментарів