Vad det är och hur man använder det

By rik

När du utvecklar applikationer med JavaScript är det troligt att du har stött på asynkrona funktioner. Exempel på dessa kan vara webbläsarens `fetch`-funktion eller Node.js `readFile`-funktion. Om du har använt dessa funktioner på samma sätt som synkrona funktioner kan du ha upplevt oväntade resultat. Detta beror på att de är asynkrona. Den här artikeln ger en förklaring av vad det innebär och hur man använder asynkrona funktioner på ett effektivt sätt.

Introduktion till synkrona funktioner

JavaScript är ett en-trådigt språk, vilket innebär att det endast kan utföra en operation åt gången. Om processorn stöter på en funktion som tar lång tid att genomföra, måste JavaScript vänta tills den funktionen är helt avklarad innan det fortsätter med andra delar av koden.

De flesta funktioner utförs helt av processorn. Det betyder att under tiden dessa funktioner exekveras, oavsett hur lång tid det tar, är processorn upptagen. Dessa funktioner kallas synkrona funktioner. Här är ett exempel:

function add(a, b) {
    for (let i = 0; i < 1000000; i ++) {
        // Gör ingenting
    }
    return a + b;
}

// Anrop av funktionen tar lång tid
summa = add(10, 5);

// Processorn kan inte gå vidare till
console.log(summa);

Den här funktionen innehåller en stor loop vars exekvering tar tid innan summan av de två argumenten returneras.

Efter att funktionen har definierats anropar vi den och sparar resultatet i variabeln `summa`. Sedan skriver vi ut värdet av `summa`. Trots att anropet till `add`-funktionen tar tid, kan inte processorn fortsätta till `console.log` förrän `add`-funktionen är klar.

De flesta funktioner som du kommer att stöta på uppför sig förutsägbart, som exemplet ovan. Vissa funktioner är dock asynkrona och fungerar annorlunda.

Introduktion till asynkrona funktioner

Asynkrona funktioner utför majoriteten av sitt arbete utanför processorn. Det innebär att även om en funktion tar tid att slutföra, kan processorn vara ledig och fortsätta med andra uppgifter.

Här är ett exempel på en sådan funktion:

fetch('https://jsonplaceholder.typicode.com/users/1');

För att maximera effektiviteten, tillåter JavaScript processorn att arbeta med andra CPU-krävande uppgifter även innan en asynkron funktion är färdig.

Eftersom processorn går vidare innan den asynkrona funktionen är klar, är dess resultat inte omedelbart tillgängligt. Det kommer att komma senare. Om processorn försökte köra kod som var beroende av det väntande resultatet, skulle det uppstå fel.

Processorn bör därför bara köra kod som inte är beroende av resultatet. För att uppnå detta, använder modern JavaScript så kallade ”löften” (promises).

Vad är ett löfte i JavaScript?

I JavaScript är ett löfte ett tillfälligt värde som returneras av en asynkron funktion. Löften är grundstenen i modern asynkron programmering i JavaScript.

När ett löfte har skapats kan två saker hända: Det antingen ”löser sig” när det önskade värdet produceras, eller så ”avvisas” det vid ett eventuellt fel. Dessa händelser utgör löftets livscykel. Vi kan koppla händelsehanterare till löftet, som anropas när löftet löses eller avvisas.

All kod som kräver det slutliga värdet av en asynkron funktion kan kopplas till löftets händelsehanterare för när det löses. All kod som hanterar fel i ett avvisat löfte kopplas till dess motsvarande händelsehanterare.

Här är ett exempel på hur man läser data från en fil i Node.js:

const fs = require('fs/promises');

fileReadPromise = fs.readFile('./hello.txt', 'utf-8');

fileReadPromise.then((data) => console.log(data));

fileReadPromise.catch((error) => console.log(error));

På första raden importerar vi modulen `fs/promises`.

På andra raden anropar vi `readFile`-funktionen och anger filnamn och kodning för den fil vars innehåll vi vill läsa. Den här funktionen är asynkron och returnerar därför ett löfte. Vi sparar löftet i variabeln `fileReadPromise`.

På tredje raden kopplar vi en händelselyssnare som ska anropas när löftet löses. Detta gör vi genom att anropa metoden `then` på löftesobjektet. Vi skickar in en funktion som ska köras när löftet löses som argument till `then`.

På fjärde raden kopplar vi en lyssnare som ska anropas när löftet avvisas. Detta görs genom att anropa metoden `catch` och skicka in en felhanteringsfunktion som argument.

Ett alternativt tillvägagångssätt är att använda nyckelorden `async` och `await`, vilket vi ska gå igenom nu.

Förklaring av Async och Await

Nyckelorden `async` och `await` används för att skriva asynkron JavaScript-kod på ett mer läsbart sätt. Vi ska nu förklara hur dessa nyckelord används och deras effekt på koden.

Nyckelordet `await` pausar exekveringen av en funktion medan den väntar på att en asynkron funktion ska slutföras. Här är ett exempel:

const fs = require('fs/promises');

function readData() {
	const data = await fs.readFile('./hello.txt', 'utf-8');

    // Denna rad kommer inte att köras förrän data är tillgänglig
	console.log(data);
}

readData()

Vi använder nyckelordet `await` när vi anropar `readFile`. Detta instruerar processorn att vänta tills filen har lästs innan nästa rad ( `console.log`) kan köras. Detta garanterar att kod som beror på resultatet av en asynkron funktion inte körs förrän resultatet är tillgängligt.

Om du försöker köra koden ovan kommer du att stöta på ett fel. Detta beror på att `await` endast kan användas inuti en asynkron funktion. För att deklarera en funktion som asynkron, använder du nyckelordet `async` före funktionsdeklarationen, så här:

const fs = require('fs/promises');

async function readData() {
	const data = await fs.readFile('./hello.txt', 'utf-8');

    // Denna rad kommer inte att köras förrän data är tillgänglig
	console.log(data);
}

// Anropar funktionen så att den körs
readData()

// Koden här kommer att köras medan vi väntar på att readData ska slutföras
console.log('Väntar på att data ska bli klar')

När du kör detta kodavsnitt, ser du att JavaScript kör den yttre `console.log` medan den väntar på att data från textfilen ska bli tillgänglig. När data är tillgänglig, körs `console.log` inuti `readData`.

Felhantering vid användning av nyckelorden `async` och `await` görs med hjälp av `try/catch` block. Det är också viktigt att veta hur man hanterar loopar med asynkron kod.

`async` och `await` är tillgängliga i modern JavaScript. Tidigare skrevs asynkron kod främst med hjälp av callbacks.

Introduktion till callbacks

En callback är en funktion som anropas när ett resultat är tillgängligt. All kod som behöver resultatet placeras inuti callback-funktionen. All kod utanför callback-funktionen beror inte på resultatet och kan därför köras fritt.

Här är ett exempel på hur man läser en fil i Node.js:

const fs = require("fs");

fs.readFile("./hello.txt", "utf-8", (err, data) => {

	// I denna callback lägger vi all kod som kräver data
	if (err) console.log(err);
	else console.log(data);
});

// Här kan vi utföra alla uppgifter som inte behöver resultatet
console.log("Hej från programmet")

På första raden importerar vi `fs`-modulen. Sedan anropar vi `readFile`-funktionen från `fs`-modulen. `readFile` läser text från den angivna filen. Det första argumentet anger filen och det andra argumentet anger filformatet.

`readFile` läser text från filer asynkront. Därför tar funktionen en funktion som argument. Det här argumentet är en callback-funktion och anropas när data har lästs.

Det första argumentet som skickas till callback-funktionen när den anropas är ett fel, som har ett värde om ett fel har inträffat. Om inget fel har uppstått är värdet `undefined`.

Det andra argumentet som skickas till callback-funktionen är data som lästs från filen. Koden i funktionen har tillgång till data från filen. Kod utanför denna funktion är inte beroende av data från filen och kan därför köras medan man väntar på data från filen.

När koden körs får vi följande resultat:

Viktiga JavaScript-funktioner

Det finns några viktiga funktioner och egenskaper som påverkar hur asynkron JavaScript fungerar. Dessa förklaras i videon nedan:

Jag har sammanfattat de två viktigaste funktionerna nedan.

#1. Enkeltrådig

Till skillnad från andra språk som tillåter programmeraren att använda flera trådar, tillåter JavaScript endast en tråd. En tråd är en sekvens av instruktioner som logiskt sett beror på varandra. Flera trådar tillåter programmet att köra en annan tråd när blockerande operationer stöts på.

Men flera trådar ökar komplexiteten och gör det svårare att förstå programmen som använder dem. Detta ökar risken för att buggar introduceras i koden och det blir svårare att felsöka koden. JavaScript gjordes en-trådigt av enkelhetsskäl. Som ett en-trådigt språk förlitar det sig på händelsestyrning för att hantera blockerande operationer effektivt.

#2. Händelsestyrd

JavaScript är också händelsestyrt. Det betyder att vissa händelser inträffar under ett JavaScript-programs livscykel. Som programmerare kan du koppla funktioner till dessa händelser, och när händelsen inträffar anropas den bifogade funktionen och körs.

Vissa händelser kan bero på att resultatet av en blockerande operation blir tillgängligt. I så fall anropas den associerade funktionen med resultatet som argument.

Saker att tänka på när du skriver asynkron JavaScript

I det sista avsnittet ska vi ta upp några aspekter som är viktiga att ha i åtanke när du skriver asynkron JavaScript-kod. Det innefattar webbläsarstöd, bästa praxis och betydelsen av asynkron kod.

Webbläsarstöd

Den här tabellen visar stöd för löften i olika webbläsare.

Källa: caniuse.com

Den här tabellen visar stöd för asynkrona nyckelord i olika webbläsare.

Källa: caniuse.com

Bästa metoder

  • Välj alltid `async/await` eftersom det hjälper dig att skriva renare kod som är lättare att förstå.
  • Hantera fel i `try/catch` block.
  • Använd nyckelordet `async` endast när det är nödvändigt att vänta på resultatet av en funktion.

Vikten av asynkron kod

Asynkron kod ger möjlighet att skriva mer effektiva program som bara använder en tråd. Det här är viktigt eftersom JavaScript används för att bygga webbplatser som gör många asynkrona operationer, såsom nätverksanrop och läsning eller skrivning av filer. Den här effektiviteten har lett till att körtider som NodeJS har blivit allt mer populära som den föredragna körmiljön för applikationsservrar.

Slutord

Detta har varit en lång artikel, men vi har lyckats gå igenom skillnaden mellan asynkrona och synkrona funktioner. Vi har även behandlat hur man använder asynkron kod genom att använda löften, nyckelorden `async/await` och callbacks.

Vi har dessutom berört de viktigaste funktionerna i JavaScript. I det sista avsnittet har vi avslutat med webbläsarstöd och bästa praxis.

Kolla även in Node.js vanliga intervjufrågor.