Så fungerar Node.js

Slutförd

I den här lektionen beskrivs hur Node.js hanterar inkommande uppgifter till JavaScript-körningen.

Typer av uppgifter

JavaScript-program har två typer av uppgifter:

  • Synkrona uppgifter: Dessa uppgifter sker i ordning. De är inte beroende av en annan resurs som ska slutföras. Exempel är matematiska operationer eller strängmanipulering.
  • Asynkron: Dessa uppgifter kanske inte slutförs omedelbart eftersom de är beroende av andra resurser. Exempel är nätverksbegäranden eller filsystemåtgärder.

Eftersom du vill att programmet ska köras så snabbt som möjligt vill du att JavaScript-motorn ska kunna fortsätta fungera medan den väntar på ett svar från en asynkron åtgärd. För att göra det läggs den asynkrona aktiviteten till i en aktivitetskö och fortsätter att arbeta med nästa uppgift.

Hantera aktivitetskö med händelseloop

Node.js använder JavaScript-motorns händelsedrivna arkitektur för att bearbeta asynkrona begäranden. Följande diagram illustrerar hur V8-händelseloopen fungerar på hög nivå:

Diagram showing how Node J S uses an event-driven architecture where an event loop processes events and returns callbacks.

En asynkron uppgift, som anges med lämplig syntax (visas nedan), läggs till i händelseloopen. Uppgiften innehåller det arbete som ska utföras och en återanropsfunktion för att ta emot resultaten. När den intensiva åtgärden är klar utlöses återanropsfunktionen med resultatet.

Synkrona åtgärder jämfört med asynkrona åtgärder

Node.js-API:erna tillhandahåller både asynkrona och synkrona åtgärder för vissa av samma åtgärder, till exempel filåtgärder. Även om du vanligtvis alltid bör tänka asynkront först, finns det tillfällen då du kan använda synkrona åtgärder.

Ett exempel är när ett kommandoradsgränssnitt (CLI) läser en fil och sedan omedelbart använder data i filen. I det här fallet kan du använda den synkrona versionen av filåtgärden eftersom det inte finns något annat system eller någon annan person som väntar på att använda programmet.

Men om du skapar en webbserver bör du alltid använda den asynkrona versionen av filåtgärden för att inte blockera körningsförmågan för en tråd att bearbeta andra användarbegäranden.

I ditt arbete som utvecklare på TailWind Traders måste du förstå skillnaden mellan synkrona och asynkrona åtgärder och när du ska använda var och en.

Prestanda via asynkrona åtgärder

Node.js drar nytta av den unika händelsedrivna karaktären hos JavaScript som gör det snabbt och högpresterande att skapa serveruppgifter. JavaScript, när det används korrekt med asynkrona tekniker, kan ge samma prestandaresultat som lågnivåspråk som C på grund av prestandaökningar som möjliggörs av V8-motorn.

De asynkrona teknikerna finns i tre formatmallar, som du måste kunna känna igen i ditt arbete:

  • Async/await (rekommenderas): Den senaste asynkrona tekniken som använder nyckelorden async och await för att ta emot resultatet av en asynkron åtgärd. Async/await används på många programmeringsspråk. I allmänhet använder nya projekt med nyare beroenden den här typen av asynkron kod.
  • Återanrop: Den ursprungliga asynkrona tekniken som använder en återanropsfunktion för att ta emot resultatet av en asynkron åtgärd. Du ser detta i äldre kodbaser och i äldre Node.js-API:er.
  • Promises: En nyare asynkron teknik som använder ett löftesobjekt för att ta emot resultatet av en asynkron åtgärd. Du ser detta i nyare kodbaser och i nyare Node.js-API:er. Du kan behöva skriva löftesbaserad kod i ditt arbete för att omsluta äldre API:er som inte uppdateras. Genom att använda löften för den här omslutningen tillåter du att koden används i ett större antal node.js-versioner än i den nyare kodstilen async/await.

Async/await

Async/await är ett nytt sätt att hantera asynkron programmering. Async/await är syntaktisk socker utöver löften och får asynkron kod att se mer ut som synkron kod. Det är också lättare att läsa och underhålla.

Samma exempel med async/await ser ut så här:

// 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);
  });

När async/await släpptes i ES2017 kunde nyckelorden bara användas i funktioner där den översta funktionen är ett löfte. Även om löftet inte behövde ha then och catch avsnitt, var det fortfarande nödvändigt att ha promise syntax för att köra.

En async funktion returnerar alltid ett löfte, även om den inte har något await anrop inuti den. Löftet matchas med det värde som returneras av funktionen. Om funktionen utlöser ett fel avvisas löftet med värdet som genereras.

Löften

Eftersom kapslade återanrop kan vara svåra att läsa och hantera har Node.js lagt till stöd för löften. Ett löfte är ett objekt som representerar det slutliga slutförandet (eller felet) av en asynkron åtgärd.

En promise-funktion har formatet:

// 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
  });

Metoden then anropas när löftet uppfylls och catch metoden anropas när löftet avvisas.

Om du vill läsa en fil asynkront med löften är koden:

// 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!`);

Asynkron/väntar på toppnivå

De senaste versionerna av Node.js har lagt till async/await på översta nivån för ES6-moduler. Du måste lägga till en egenskap med namnet type i package.json med värdet module för för att använda den här funktionen.

{
    "type": "module"
}

Sedan kan du använda nyckelordet await på den översta nivån i koden

// 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!");

Återanrop

När Node.js ursprungligen släpptes hanterades asynkron programmering med hjälp av återanropsfunktioner. Återanrop är funktioner som skickas som argument till andra funktioner. När uppgiften är klar anropas återanropsfunktionen.

Ordningen på parametrarna för funktionen är viktig. Återanropsfunktionen är den sista parametern för funktionen.

// Callback function is the last parameter
function(param1, param2, paramN, callback)

Funktionsnamnet i koden som du underhåller kanske inte anropas callback. Det kan anropas cb eller done .next Namnet på funktionen är inte viktigt, men parametrarnas ordning är viktig.

Observera att det inte finns någon syntaktisk indikation på att funktionen är asynkron. Du måste veta att funktionen är asynkron genom att läsa dokumentationen eller fortsätta att läsa igenom koden.

Motringningsexempel med namngiven återanropsfunktion

Följande kod separerar funktionen async från återanropet. Det här är lätt att läsa och förstå och gör att du kan återanvända återanropet för andra asynkrona funktioner.

// 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!");

Rätt resultat är:

I'm the last line of the file!
Hi, developers!
Done!

Först startas den asynkrona funktionen fs.readFile och går in i händelseloopen. Sedan fortsätter kodkörningen till nästa kodrad, som är den sista console.log. När filen har lästs anropas återanropsfunktionen och de två console.log-uttrycken körs.

Motringningsexempel med anonym funktion

I följande exempel används en anonym återanropsfunktion, vilket innebär att funktionen inte har något namn och inte kan återanvändas av andra anonyma funktioner.

// 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!");

Rätt resultat är:

I'm the last line of the file!
Hi, developers!
Done!

När koden körs startas den asynkrona funktionen fs.readFile och hamnar i händelseloopen. Därefter fortsätter körningen till följande kodrad, som är den sista console.log. När filen läss anropas återanropsfunktionen och de två console.log-uttrycken körs.

Kapslade återanrop

Eftersom du kan behöva anropa ett efterföljande asynkront återanrop och sedan ett annat kan återanropskoden bli kapslad. Detta kallas återanropshelvete och är svårt att läsa och underhålla.

// 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
          }
        });
      }
    });
  }
});

Synkrona API:er

Node.js har också en uppsättning synkrona API:er. Dessa API:er blockerar körningen av programmet tills uppgiften har slutförts. Synkrona API:er är användbara när du vill läsa en fil och sedan omedelbart använda data i filen.

Synkrona (blockerande) funktioner i Node.js använder namngivningskonventionen functionSyncför . Det asynkrona readFile API:et har till exempel en synkron motsvarighet med namnet readFileSync. Det är viktigt att upprätthålla den här standarden i dina egna projekt så att koden är lätt att läsa och förstå.

// 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);
}

Som ny utvecklare på TailWind Traders kan du bli ombedd att ändra valfri typ av Node.js-kod. Det är viktigt att förstå skillnaden mellan synkrona och asynkrona API:er och de olika syntaxerna för asynkron kod.