Пошук уроків, статей та іншого контенту
З’ясуємо, як Node.js завантажує код, створює контекст виконання та обробляє синхронні операції.
Коли ми запускаємо файл командою:
node app.jsNode.js виконує кілька основних кроків:
знаходить файл app.js;
читає його вміст;
передає JavaScript-код рушію V8;
створює контекст виконання;
виконує інструкції зверху вниз;
завершує процес після виконання всіх синхронних операцій.
Node.js не є окремою мовою програмування. Це середовище виконання JavaScript поза браузером.
До складу Node.js входять:
стандартні модулі Node.js;
механізми роботи з файлами, мережею та процесами;
середовище виконання, у якому JavaScript-код може взаємодіяти з операційною системою.
Файл, переданий команді node, стає точкою входу програми.
Наприклад, створимо файл app.js:
console.log("Початок програми");
const firstNumber = 10;
const secondNumber = 20;
const result = firstNumber + secondNumber;
console.log(`Результат: ${result}`);
console.log("Кінець програми");Запустимо його:
node app.jsРезультат:
Початок програми
Результат: 30
Кінець програмиNode.js читає файл і виконує його інструкції в тому порядку, у якому вони записані.
У цьому прикладі:
виводиться "Початок програми";
створюються змінні;
обчислюється сума;
виводиться результат;
виводиться "Кінець програми".
JavaScript-код не виконується безпосередньо операційною системою. Node.js передає його рушію V8.
V8:
аналізує JavaScript-код;
перетворює його у внутрішнє представлення;
виконує інструкції;
керує пам’яттю та об’єктами JavaScript.
Для розробника важливо розуміти практичний результат: Node.js запускає JavaScript у середовищі V8, але додає до нього можливості, яких зазвичай немає в браузері, наприклад роботу з файловою системою.
Контекст виконання — це середовище, у якому JavaScript виконує код.
У контексті виконання зберігається інформація про:
доступні змінні;
функції;
поточне значення this;
місце, з якого потрібно продовжити виконання;
зовнішнє оточення змінних.
Кожна JavaScript-програма має початковий контекст виконання. Коли викликається функція, для неї створюється окремий контекст.
Розглянемо приклад:
const message = "Глобальне повідомлення";
function showMessage() {
const localMessage = "Локальне повідомлення";
console.log(message);
console.log(localMessage);
}
showMessage();Під час запуску відбувається таке:
Node.js створює контекст для основного файлу.
У ньому створюється змінна message і функція showMessage.
Викликається showMessage().
Для функції створюється новий контекст виконання.
У ньому створюється localMessage.
Функція отримує доступ і до власної змінної, і до змінної із зовнішнього контексту.
Після завершення функції її контекст більше не використовується.
Змінна localMessage доступна лише всередині функції:
function showMessage() {
const localMessage = "Локальне повідомлення";
console.log(localMessage);
}
showMessage();
// Помилка: localMessage недоступна за межами функції
console.log(localMessage);Змінні, оголошені через let і const, мають блочну область видимості. Це означає, що вони доступні лише всередині відповідного блоку або функції.
JavaScript виконує синхронний код за допомогою стеку викликів.
Стек працює за принципом:
останній доданий елемент буде виконаний першим.
Коли викликається функція, вона додається до стеку. Коли функція завершується, вона забирається зі стеку.
function first() {
console.log("Початок first");
second();
console.log("Кінець first");
}
function second() {
console.log("Виконується second");
}
first();
console.log("Програма завершена");Результат:
Початок first
Виконується second
Кінець first
Програма завершенаПорядок виконання:
Node.js починає виконувати основний код.
Викликається first().
first() додається до стеку викликів.
Усередині first() викликається second().
second() виконується повністю.
second() забирається зі стеку.
Виконання продовжується у first().
first() завершується.
Виконується останній console.log.
Поки функція перебуває у стеку, Node.js не переходить до наступної синхронної інструкції.
Синхронна операція виконується послідовно: наступна інструкція чекає завершення поточної.
console.log("Крок 1");
const number = 5 * 2;
console.log(`Крок 2: ${number}`);
console.log("Крок 3");Node.js не може виконати console.log("Крок 3"), доки не завершить обчислення 5 * 2 і попередні інструкції.
Для простих операцій це відбувається дуже швидко. Проте довга синхронна операція може затримати виконання всього іншого коду.
console.log("Початок");
let total = 0;
for (let index = 0; index < 1_000_000_000; index += 1) {
total += index;
}
console.log("Цикл завершено");
console.log("Кінець");Поки цикл не завершиться, Node.js не зможе виконати останні два console.log.
Це називають блокуванням виконання: поточна синхронна операція займає виконання, тому наступний код чекає.
У реальних програмах такими операціями можуть бути:
дуже великі обчислення;
обробка великих обсягів даних у пам’яті;
синхронне читання великих файлів.
На початковому етапі важливо запам’ятати: синхронний код виконується послідовно і може затримувати весь подальший код.
Node.js має вбудований модуль fs для роботи з файловою системою. Метод readFileSync читає файл синхронно.
Створимо два файли.
message.txt:
Це повідомлення з файлу.app.js:
const fs = require("node:fs");
console.log("Початок читання");
const message = fs.readFileSync("message.txt", "utf8");
console.log("Вміст файлу:");
console.log(message);
console.log("Кінець програми");Запуск:
node app.jsРезультат:
Початок читання
Вміст файлу:
Це повідомлення з файлу.
Кінець програмиРядок:
const message = fs.readFileSync("message.txt", "utf8");завершиться лише після того, як файл буде прочитано або виникне помилка. До цього Node.js не виконає наступний console.log.
Другий аргумент "utf8" вказує, що вміст файлу потрібно повернути як текст. Без нього результатом буде об’єкт типу Buffer, який представляє байти файлу.
Якщо файл не існує, readFileSync завершується помилкою, а виконання програми припиняється:
const fs = require("node:fs");
const message = fs.readFileSync("unknown.txt", "utf8");
console.log(message);Node.js виведе помилку, тому що файл unknown.txt не знайдено.
Синхронну операцію можна обгорнути в try...catch:
const fs = require("node:fs");
try {
const message = fs.readFileSync("message.txt", "utf8");
console.log("Вміст файлу:");
console.log(message);
} catch (error) {
console.error("Не вдалося прочитати файл");
}Якщо читання завершиться помилкою, керування перейде до блоку catch, а програма зможе обробити цю ситуацію.
У типовій програмі Node.js JavaScript-файл розглядається як модуль. Код одного модуля не стає автоматично змінними іншого модуля.
Наприклад, у файлі data.js:
const value = 42;змінна value не буде доступною в app.js, доки ми явно її не експортуємо.
data.js:
const value = 42;
module.exports = value;app.js:
const value = require("./data");
console.log(value);Під час виконання app.js Node.js:
завантажує головний файл;
бачить виклик require("./data");
знаходить файл data.js;
виконує його код;
отримує значення module.exports;
записує це значення у змінну value;
продовжує виконання app.js.
Результат:
42Отже, під час завантаження модуля його верхньорівневий код виконується. Якщо модуль завантажується повторно, Node.js зазвичай використовує вже завантажений результат, а не виконує весь файл заново.
Розглянемо повний приклад:
const fs = require("node:fs");
console.log("1. Файл почав виконуватися");
function readMessage() {
console.log("2. Функцію викликано");
const message = fs.readFileSync("message.txt", "utf8");
console.log("3. Файл прочитано");
return message;
}
const message = readMessage();
console.log("4. Повідомлення:", message);
console.log("5. Програма завершила синхронну частину");Якщо message.txt містить Привіт!, результат буде таким:
1. Файл почав виконуватися
2. Функцію викликано
3. Файл прочитано
4. Повідомлення: Привіт!
5. Програма завершила синхронну частинуТут добре видно послідовність:
код верхнього рівня починає виконуватися;
оголошення функції саме по собі не виконує її тіло;
тіло функції виконується лише після readMessage();
синхронне читання завершується до переходу до наступного рядка;
після повернення результату виконується наступний код.
function greet() {
console.log("Привіт");
}Цей код лише створює функцію. Щоб виконати її тіло, потрібно викликати функцію:
greet();const result = calculate();У цьому рядку спочатку викликається calculate(), а вже потім повернуте значення записується у result.
Node.js виконує синхронні інструкції послідовно. Довга операція затримує виконання наступних інструкцій.
У прикладі:
fs.readFileSync("message.txt", "utf8");шлях "message.txt" визначається відносно поточної робочої директорії, з якої запущено команду node.
Тому файл має бути доступним за цим шляхом під час запуску програми.
Файл може бути відсутнім або недоступним. Синхронна операція читання в такому разі створює помилку, яку потрібно обробити за допомогою try...catch, якщо програма має продовжити роботу.
Node.js запускає JavaScript-файл, передаючи його рушію V8.
Файл виконується зверху вниз.
Під час виконання JavaScript створює контексти для основного коду та викликаних функцій.
Функції виконуються через стек викликів.
Синхронна операція завершується до початку наступної інструкції.
Довгі синхронні операції блокують подальше виконання коду.
Модуль завантажується та виконується під час виклику require.
Помилки синхронних операцій можна обробляти за допомогою try...catch.