Om du bara kommer igång med programmering, är chansen stor att du tänker på program som en uppsättning av sekventiella logiska block, där varje block gör en specifik sak och skickar sitt resultat så att nästa block kan köras och så vidare, och för För det mesta har du rätt, de flesta program körs på ett sekventiellt sätt, den här modellen låter oss bygga program som är enkla att skriva och underhålla. Det finns dock specifika användningsfall där denna sekventiella modell inte skulle fungera, eller inte skulle vara optimal. Som ett exempel, överväg ett bokläsarprogram. Denna applikation har några avancerade funktioner som att hitta alla förekomster av ett ord, navigera mellan bokmärken och liknande. Föreställ dig nu att användaren för närvarande läser en lång bok och bestämmer sig för att leta efter alla förekomster av ett vanligt ord som "The". Applikationen tar normalt ett par sekunder att hitta och indexera alla förekomster av det ordet. I ett sekventiellt program kan användaren inte interagera med applikationen (ändra sida eller markera en text) förrän sökoperationen är klar. Förhoppningsvis kan du se att det inte är en optimal användarupplevelse!
Diagrammet illustrerar ett typiskt exekveringsflöde för bokläsarapplikationen. Om användaren initierar en långvarig operation (i det här fallet sökningen efter alla förekomster av "den" i en stor bok), "fryser" applikationen under hela operationen. I det här fallet kommer användaren att fortsätta klicka på nästa bokmärkesknapp utan resultat tills sökoperationen är klar och alla åtgärder kommer att träda i kraft på en gång, vilket ger slutanvändaren en känsla av att applikationen släpar efter.
Du kanske har märkt att det här exemplet inte riktigt motsvarar den sekventiella modellen vi introducerade tidigare. Det beror på att verksamheterna här är oberoende av varandra. Användaren behöver inte veta om antalet förekomster av "the" för att navigera till nästa bokmärke, så ordningen för utförande av operationer är inte riktigt viktig. Vi behöver inte vänta på slutet av sökoperationen innan vi kan navigera till nästa bokmärke. En möjlig förbättring av det tidigare exekveringsflödet baseras på denna logik: vi kan köra den långa sökoperationen i bakgrunden, fortsätta med alla inkommande operationer och när den långa operationen är klar kan vi helt enkelt meddela användaren. Utförandeflödet blir som följer:
Med detta exekveringsflöde förbättras användarupplevelsen avsevärt. Nu kan användaren initiera en långvarig operation, fortsätta att använda programmet normalt och få ett meddelande när operationen är klar. Detta är grunden för asynkron programmering.
Javascript, bland andra språk, stöder denna stil av asynkron programmering genom att tillhandahålla omfattande API:er för att uppnå nästan alla asynkrona beteenden du kan tänka dig. I slutet av dagen bör Javascript vara ett asynkront språk. Om vi hänvisar till föregående exempel är den asynkrona logiken basen för alla användarinteraktionsapplikationer, och Javascript byggdes främst för att användas på webbläsaren där de flesta av programmen handlar om att svara på användaråtgärder.
Följande ger dig en kort guide om asynkront Javascript:
Återuppringningar
I ett typiskt program hittar du vanligtvis ett antal funktioner. För att använda en funktion kallar vi den med en uppsättning parametrar. Funktionskoden kommer att exekvera och returnera ett resultat, inget utöver det vanliga. Asynkron programmering förskjuter denna logik något. Om vi går tillbaka till exemplet med bokläsarapplikationen kan vi inte använda en vanlig funktion för att implementera sökoperationslogiken eftersom operationen tar en okänd tid. En vanlig funktion kommer i princip tillbaka innan operationen är klar, och det är inte det beteende vi förväntar oss. Lösningen är att specificera en annan funktion som kommer att exekveras när sökoperationen är klar. Detta modellerar vårt användningsfall eftersom vårt program kan fortsätta sitt flöde normalt och när sökoperationen är klar kommer den angivna funktionen att köras för att meddela användaren om sökresultaten. Denna funktion är vad vi kallar en callback-funktion:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Först definierar vi sökfunktionen, searchOccurrences. Det tar ordet att söka efter och en andra parameter "återuppringning" som kommer att vara funktionen att utföra när sökningen är klar. Sökoperationsfunktionen hölls avsiktligt abstrakt, vi behöver bara fokusera på dess två möjliga utfall: det första fallet är när allt gick framgångsrikt och vi har resultatet av sökningen i resultatvariabeln. I det här fallet måste vi bara anropa callback-funktionen med följande parametrar: den första parametern är null vilket betyder att inget fel har inträffat, den andra parametern är ordet som söktes och den tredje och kanske viktigaste parametern av de tre, är resultatet av sökoperationen.
Det andra fallet är där ett fel uppstår, detta är också ett fall där exekveringen av sökoperationen är klar och vi måste anropa återuppringningsfunktionen. Vi använder ett försök och fånga block för att fånga upp eventuella fel och vi anropar bara callback-funktionen med felobjektet från catch-blocket.
Vi definierade sedan callback-funktionen, handleSearchOccurrences, vi höll dess logik ganska enkel. Det är bara att skriva ut ett meddelande till konsolen. Vi kontrollerar först parametern "err" för att se om något fel uppstod i huvudfunktionen. I så fall låter vi bara användaren veta att sökoperationen slutade med ett fel. Om inga fel uppstod skriver vi ut ett meddelande med resultatet av sökoperationen.
Slutligen kallar vi funktionen för sökningOccurrences med ordet "the". Funktionen kommer nu att köras normalt utan att blockera huvudprogrammet och när sökningen är klar kommer återuppringningen att utföras och vi får resultatmeddelandet antingen med sökresultatet eller felmeddelandet.
Det är viktigt att nämna här att vi bara har tillgång till resultatvariabeln i huvud- och återuppringningsfunktionerna. Om vi försöker något sånt här:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
resultatet av utskriften skulle vara odefinierat eftersom programmen inte väntar på att funktionen searchOccurrences ska köras. Den flyttar till nästa instruktion som är print-satsen innan resultatvariabeln tilldelas inuti huvudfunktionen. Som ett resultat kommer vi att skriva ut den otilldelade resultatvariabeln.
Så baserat på denna logik bör vi behålla all kod som använder resultatvariabeln inuti callback-funktionen. Detta kanske inte verkar vara ett problem nu men det kan snabbt eskalera till ett verkligt problem. Föreställ dig fallet där vi har en kedja av asynkrona funktioner som måste köras i sekvens. I den typiska callback-logiken skulle du implementera något så här:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Tänk på att varje återuppringning har en felparameter och varje fel måste hanteras separat. Detta gör den redan komplexa koden ovan ännu mer komplex och svår att underhålla. Förhoppningsvis är Promises här för att lösa problemet med återuppringningshelvetet, vi kommer att täcka det härnäst.
Löften
Löften byggs ovanpå callbacks och fungerar på ett liknande sätt. De introducerades som en del av ES6-funktioner för att lösa några av de uppenbara problemen med återuppringningar som till exempel återuppringningshelvetet. Löften tillhandahåller sina egna funktioner som körs vid framgångsrikt slutförande (lösa), och när fel uppstår (avvisa). Följande visar exemplet searchOccurrences implementerat med löften:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Låt oss gå igenom ändringarna vi tillämpade:
SearchOccurrences-funktionen returnerar ett löfte. Inuti löftet håller vi samma logik: vi har två funktioner lösa och avvisa som representerar våra callbacks snarare än att ha en enda callback-funktion som hanterar både en framgångsrik exekvering och en exekvering med fel. Löften skiljer de två resultaten åt och ger en ren syntax när huvudfunktionen anropas. Lösningsfunktionen är "ansluten" till huvudfunktionen med hjälp av nyckelordet "då". Här anger vi bara de två parametrarna för resolve-funktionen och skriver ut sökresultatet. En liknande sak gäller för avvisningsfunktionen, den kan kopplas med nyckelordet "fånga". Förhoppningsvis kan du uppskatta fördelarna som löften erbjuder när det gäller kodläsbarhet och renlighet. Om du fortfarande diskuterar det, kolla in hur vi kan lösa problemet med återuppringningshelvetet genom att kedja ihop de asynkrona funktionerna för att köra en efter en:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Async/Await
Async/Await är det senaste tillskottet till vårt asynkrona verktygsbälte i Javascript. Introducerade med ES8 ger de ett nytt lager av abstraktion ovanpå asynkrona funktioner genom att helt enkelt "vänta" på exekvering av en asynkron operation. Flödet av programblocken vid den instruktionen tills ett resultat returneras från den asynkrona operationen och sedan kommer programmet att fortsätta med nästa instruktion. Om du tänker på det synkrona exekveringsflödet har du rätt. Vi har kommit i full cirkel! Async/await-försök att föra enkelheten med synkron programmering till den asynkrona världen. Tänk på att detta endast uppfattas i programmets körning och kod. Allt förblir detsamma under huven, Async/await använder fortfarande löften och återuppringningar är deras byggstenar.
Låt oss gå igenom vårt exempel och implementera det med Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Vår kod förändrades inte mycket, det viktiga att lägga märke till här är nyckelordet "async" före funktionsdeklarationen searchOccurrences. Detta indikerar att funktionen är asynkron. Lägg också märke till nyckelordet "avvakta" när du anropar sökfunktionen. Detta kommer att instruera programmet att vänta på exekveringen av funktionen tills resultatet returneras innan programmet kan flytta till nästa instruktion, med andra ord, resultatvariabeln kommer alltid att hålla det returnerade värdet av funktionen searchOccurrences och inte löftet om funktionen, i den meningen har Async/Await inte ett väntande tillstånd som Promises. När exekveringen är klar, går vi till utskriftssatsen och den här gången innehåller resultatet faktiskt resultatet av sökoperationen. Som förväntat har den nya koden samma beteende som om den vore synkron.
En annan mindre sak att tänka på är att eftersom vi inte längre har återuppringningsfunktioner, måste vi hantera searchOccurrences-felet i samma funktion eftersom vi inte bara kan sprida felet till återuppringningsfunktionen och hantera det där. Här skriver vi bara ut ett felmeddelande vid ett fel för exemplets skull.
Sammanfatta
I den här artikeln gick vi igenom de olika metoderna som används för att implementera asynkron logik i Javascript. Vi började med att utforska ett konkret exempel på varför vi skulle behöva byta från den vanliga synkrona stilen av programmering till den asynkrona modellen. Vi gick sedan över till callbacks, som är de viktigaste byggstenarna i asynkront Javascript. Begränsningarna för återuppringningar ledde oss till de olika alternativen som lagts till under åren för att övervinna dessa begränsningar, främst löften och Async/await. Asynkron logik kan hittas var som helst på webben, oavsett om du anropar ett externt API, initierar en databasfråga, skriver till det lokala filsystemet eller till och med väntar på användarinmatning på ett inloggningsformulär. Förhoppningsvis känner du dig nu mer säker på att ta itu med dessa problem genom att skriva rent och underhållbart asynkront Javascript!
Om du gillar den här artikeln, vänligen kolla in CLA-bloggen där vi diskuterar olika ämnen om hur man kommer in i tekniken. Kolla även in vår youtube-kanal för våra tidigare kostnadsfria workshops och följ oss på sociala medier så att du inte missar kommande!