Jak działa środowisko Node.js
W tej lekcji wyjaśniono, jak Node.js obsługuje zadania przychodzące do środowiska uruchomieniowego języka JavaScript.
Typy zadań
Aplikacje JavaScript mają dwa typy zadań:
- Zadania synchroniczne: te zadania są wykonywane w kolejności. Nie są one zależne od innego zasobu do ukończenia. Przykłady to operacje matematyczne lub manipulowanie ciągami.
- Asynchroniczne: te zadania mogą nie zostać wykonane natychmiast, ponieważ są zależne od innych zasobów. Przykłady to żądania sieciowe lub operacje systemu plików.
Ponieważ chcesz, aby program działał tak szybko, jak to możliwe, chcesz, aby aparat JavaScript mógł kontynuować pracę, czekając na odpowiedź z operacji asynchronicznej. Aby to zrobić, dodaje zadanie asynchroniczne do kolejki zadań i kontynuuje pracę nad następnym zadaniem.
Zarządzanie kolejką zadań za pomocą pętli zdarzeń
Node.js używa architektury sterowanej zdarzeniami aparatu JavaScript do przetwarzania żądań asynchronicznych. Na poniższym diagramie przedstawiono działanie pętli zdarzeń V8 na wysokim poziomie:
Zadanie asynchroniczne oznaczone odpowiednią składnią (pokazaną poniżej) jest dodawane do pętli zdarzeń. Zadanie obejmuje pracę, która ma zostać wykonana, oraz funkcję wywołania zwrotnego w celu odbierania wyników. Po zakończeniu operacji intensywnej wywołanie zwrotne zostanie wyzwolone z wynikami.
Operacje synchroniczne a operacje asynchroniczne
Interfejsy API Node.js zapewniają zarówno operacje asynchroniczne, jak i synchroniczne dla niektórych z tych samych operacji, takich jak operacje na plikach. Ogólnie rzecz biorąc, zawsze należy myśleć asynchronicznie, ale czasami mogą być używane operacje synchroniczne.
Przykładem jest, gdy interfejs wiersza polecenia odczytuje plik, a następnie natychmiast używa danych w pliku. W takim przypadku można użyć synchronicznej wersji operacji pliku, ponieważ nie ma innego systemu ani osoby oczekującej na użycie aplikacji.
Jeśli jednak tworzysz serwer internetowy, zawsze należy używać asynchronicznej wersji operacji pliku, aby nie blokować możliwości wykonywania pojedynczego wątku w celu przetwarzania innych żądań użytkowników.
W swojej pracy jako deweloper w firmie TailWind Traders musisz zrozumieć różnicę między operacjami synchronicznymi i asynchronicznymi oraz kiedy należy ich używać.
Wydajność za pomocą operacji asynchronicznych
Node.js wykorzystuje unikatowy charakter oparty na zdarzeniach języka JavaScript, który sprawia, że tworzenie zadań serwera jest szybkie i wydajne. Język JavaScript, jeśli jest używany poprawnie z technikami asynchronicznymi, może generować takie same wyniki wydajności jak języki niskiego poziomu, takie jak C , ze względu na zwiększenie wydajności, które umożliwił aparat V8.
Techniki asynchroniczne są dostępne w 3 stylach, które należy rozpoznać w pracy:
- Asynchroniczna/await (zalecana): najnowsza technika asynchroniczna, która używa
async
słów kluczowych iawait
do odbierania wyników operacji asynchronicznej. Funkcja Async/await jest używana w wielu językach programowania. Ogólnie rzecz biorąc, nowe projekty z nowszymi zależnościami będą używać tego stylu kodu asynchronicznego. - Wywołania zwrotne: oryginalna technika asynchroniczna, która używa funkcji wywołania zwrotnego do odbierania wyników operacji asynchronicznej. Zobaczysz to w starszych bazach kodu i w starszych interfejsach API Node.js.
- Obietnice: Nowsza technika asynchroniczna, która używa obiektu obietnicy do odbierania wyników operacji asynchronicznej. Zobaczysz to w nowszych bazach kodu i nowszych interfejsach API Node.js. Może być konieczne napisanie kodu opartego na obietnicach w pracy w celu opakowania starszych interfejsów API, które nie zostaną zaktualizowane. Korzystając z obietnic dla tego zawijania, można zezwolić na użycie kodu w większym zakresie projektów w wersji Node.js niż w nowszym stylu async/await kodu.
Async/await
Async/await to najnowszy sposób obsługi programowania asynchronicznego. Async/await to cukier syntatyczny na szczycie obietnic i sprawia, że kod asynchroniczny wygląda bardziej jak kod synchroniczny. Łatwiej jest również odczytywać i konserwować.
Ten sam przykład użycia async/await wygląda następująco:
// async/await asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// `async` before the parent function
async function readFileAsync() {
try {
// `await` before the async method
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
}
readFileAsync()
.then(() => {
console.log('Success!');
})
.catch((error) => {
console.log('An error occurred...: ', error);
});
Gdy async/await został wydany w ES2017, słowa kluczowe mogą być używane tylko w funkcjach z funkcją najwyższego poziomu jest obietnicą. Chociaż obietnica nie musiała mieć then
i catch
sekcje, nadal trzeba było mieć promise
składnię do uruchomienia.
async
Funkcja zawsze zwraca obietnicę, nawet jeśli nie ma w niej wywołaniaawait
. Obietnica zostanie rozwiązana z wartością zwróconą przez funkcję. Jeśli funkcja zgłosi błąd, obietnica zostanie odrzucona z zwróconą wartością.
Obietnice
Ponieważ zagnieżdżone wywołania zwrotne mogą być trudne do odczytania i zarządzania, Node.js dodano obsługę obietnic. Obietnica to obiekt reprezentujący ukończenie ostateczne (lub niepowodzenie) operacji asynchronicznej.
Funkcja promise ma format:
// Create a basic promise function
function promiseFunction() {
return new Promise((resolve, reject) => {
// do something
if (error) {
// indicate success
reject(error);
} else {
// indicate error
resolve(data);
}
});
}
// Call a basic promise function
promiseFunction()
.then((data) => {
// handle success
})
.catch((error) => {
// handle error
});
Metoda then
jest wywoływana po spełnieniu obietnicy, a catch
metoda jest wywoływana po odrzuceniu obietnicy.
Aby odczytać plik asynchronicznie z obietnicami, kod to:
// promises asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// request to read a file
fs.readFile(filePath, 'utf-8')
.then((data) => {
console.log(data);
console.log('Done!');
})
.catch((error) => {
console.log('An error occurred...: ', error);
});
console.log(`I'm the last line of the file!`);
Async/await najwyższego poziomu
Najnowsze wersje Node.js dodano async/await najwyższego poziomu dla modułów ES6. Musisz dodać właściwość o nazwie type
w package.json z wartością module
, aby użyć tej funkcji.
{
"type": "module"
}
Następnie możesz użyć await
słowa kluczowego na najwyższym poziomie kodu.
// top-level async/await asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// `async` before the parent function
try {
// `await` before the async method
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");
Wywołania zwrotne
Po wydaniu Node.js programowanie asynchroniczne było obsługiwane przy użyciu funkcji wywołania zwrotnego. Wywołania zwrotne to funkcje, które są przekazywane jako argumenty do innych funkcji. Po zakończeniu zadania wywoływana jest funkcja wywołania zwrotnego.
Kolejność parametrów funkcji jest ważna. Funkcja wywołania zwrotnego jest ostatnim parametrem funkcji.
// Callback function is the last parameter
function(param1, param2, paramN, callback)
Nazwa funkcji w kodzie, który utrzymujesz, może nie być wywoływana .callback
Może być wywoływany cb
lub done
.next
Nazwa funkcji nie jest ważna, ale kolejność parametrów jest ważna.
Zwróć uwagę, że nie ma żadnych wskazówek syntatycznych, że funkcja jest asynchroniczna. Musisz wiedzieć, że funkcja jest asynchroniczna, czytając dokumentację lub kontynuując czytanie kodu.
Przykład wywołania zwrotnego z nazwaną funkcją wywołania zwrotnego
Poniższy kod oddziela funkcję async od wywołania zwrotnego. Jest to łatwe do odczytania i zrozumienia oraz umożliwia ponowne użycie wywołania zwrotnego dla innych funkcji asynchronicznych.
// callback asynchronous example
// file system module from Node.js
const fs = require('fs');
// relative path to file
const filePath = './file.txt';
// callback
const callback = (error, data) => {
if (error) {
console.log('An error occurred...: ', error);
} else {
console.log(data); // Hi, developers!
console.log('Done!');
}
};
// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);
console.log("I'm the last line of the file!");
Prawidłowy wynik to:
I'm the last line of the file!
Hi, developers!
Done!
Najpierw funkcja asynchroniczna jest uruchamiana fs.readFile
i przechodzi do pętli zdarzeń. Następnie wykonanie kodu jest kontynuowane do następnego wiersza kodu, czyli ostatniego console.log
. Po odczytaniu pliku wywoływana jest funkcja wywołania zwrotnego, a dwie instrukcje console.log są wykonywane.
Przykład wywołania zwrotnego z funkcją anonimową
W poniższym przykładzie użyto funkcji anonimowego wywołania zwrotnego, co oznacza, że funkcja nie ma nazwy i nie może być ponownie używana przez inne funkcje anonimowe.
// callback asynchronous example
// file system module from Node.js
const fs = require('fs');
// relative path to file
const filePath = './file.txt';
// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
if (error) {
console.log('An error occurred...: ', error);
} else {
console.log(data); // Hi, developers!
console.log('Done!');
}
});
console.log("I'm the last line of the file!");
Prawidłowy wynik to:
I'm the last line of the file!
Hi, developers!
Done!
Po wykonaniu kodu funkcja fs.readFile
asynchroniczna jest uruchamiana i przechodzi do pętli zdarzeń. Następnie wykonanie będzie kontynuowane do następującego wiersza kodu, czyli ostatniego console.log
. Po odczytaniu pliku wywoływana jest funkcja wywołania zwrotnego, a dwie instrukcje console.log są wykonywane.
Zagnieżdżone wywołania zwrotne
Ze względu na to, że może być konieczne wywołanie kolejne wywołania zwrotnego asynchronicznego, a następnie inny kod wywołania zwrotnego może zostać zagnieżdżony. Jest to nazywane piekłem wywołania zwrotnego i jest trudne do odczytania i utrzymania.
// nested callback example
// file system module from Node.js
const fs = require('fs');
fs.readFile(param1, param2, (error, data) => {
if (!error) {
fs.writeFile(paramsWrite, (error, data) => {
if (!error) {
fs.readFile(paramsRead, (error, data) => {
if (!error) {
// do something
}
});
}
});
}
});
Synchroniczne interfejsy API
Node.js ma również zestaw synchronicznych interfejsów API. Te interfejsy API blokują wykonywanie programu do momentu ukończenia zadania. Synchroniczne interfejsy API są przydatne, gdy chcesz odczytać plik, a następnie natychmiast użyć danych w pliku.
Funkcje synchroniczne (blokujące) w Node.js używają konwencji nazewnictwa .functionSync
Na przykład interfejs API asynchroniczny readFile
ma synchroniczny odpowiednik o nazwie readFileSync
. Ważne jest, aby zachować ten standard we własnych projektach, aby kod był łatwy do odczytania i zrozumienia.
// synchronous example
const fs = require('fs');
const filePath = './file.txt';
try {
// request to read a file
const data = fs.readFileSync(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
Jako nowy deweloper w firmie TailWind Traders możesz zostać poproszony o zmodyfikowanie dowolnego typu kodu Node.js. Ważne jest, aby zrozumieć różnicę między synchronicznymi i asynchronicznymi interfejsami API oraz różnymi składniami kodu asynchronicznego.