# 3 former av asynkron bearbetning i JavaScript

6 september 2022 | https://www.ribomation.se/blog/2022/3-former-av-asynkron-bearbetning-i-javascript/

Vad innebär asynkron bearbetning/hantering (eng. _asynchronous computation_)? I korthet, innebär det att anropa en funktion vid ett senare tillfälle när resultatet av en operation (t.ex. ett musklick eller ett HTTP anrop) är klart.

Funktionen som anropas brukar formellt kallas för en _continuation_, medan den informellt kallas för en _call-back function_. Programspråk och programbibliotek med stöd för _continuation-passing style_ har funnits under lång tid. Själva begreppet daterar sig tillbaka till 1964, men kom att formaliseras under 1970-talet.

Då JavaScript skapades
----------------------

När Brendan Eich anställdes på Netscape i april 1995, fick han 10 dagar på sig att designa ett nytt programmeringsspråk för deras, vid den tiden, populära webbläsare. Det fanns givetvis en hel del begränsningar att ta hänsyn till, men den av dessa som helt kom att forma det nya språkets exekveringsmodell, var bristen på fler-trådad exekvering (eng. _multi-threading_). Språket i fråga, var givetvis JavaScript.

Fler-trådad exekvering används bl.a. i språket Java för att hantera flera samtidigt pågående operationer. Lösningen för Brendan blev att använda sig av continuation funktioner för varje operation som på ett eller annat sätt involverade väntetid.

Hur JavaScript exekverar
------------------------

De två centrala delarna av JavaScript motorn (eng. _JS virtual machine_), som hanterar exekveringen är

1.  _Call-back Queue_
2.  _Function Execution Stack_

Stacken fungerar på ett liknande sätt som för konventionella programspråk, d.v.s. en anropad funktion lägger till överst (eng. _push stack_) en beskrivning av funktionens lokala variabler och återhopps-information (eng. _return address_). Detta kallas på engelska för en _stack-frame_. Om aktuell funktion anropar en annan funktion, så läggs det en ny stack-frame överst, och så vidare. När en funktion returnerar, så tas motsvarande stack-frame bort (eng. _pop stack_).

För konventionella programspråk, så avslutas exekveringen när stacken är tom. Så icke för JS, utan då hämtas nästa funktion för exekvering från call-back queue. Så där håller det på tills, både stack och kö är tom, då först avslutas programmet.

Detta gäller när JS exekveras på kommando-raden, typiskt då som ett Node.js program. När ett JS program emellertid exekveras i en webbläsare, så ligger programmet och väntar på att någon händelse ska inträffa, såsom musklick, tangentbordsklick eller att en påbörjad operation, såsom HTTP, är avslutad.

Call-back
---------

Ett exempel på användning av call-back är fördröjt funktionsanrop med `setTimeout()`. Exemplet nedan visar hur man fördröjer en utskrift med en 1 sekund (1000 ms).

```js
setTimeout(() => { console.log('Tjabba Habba'); }, 1000);
```

Nästa exempel visar hur man fyller på call-back queue med rekursiva funktioner. En intervall timer startas, vilken registrerar en call-back varje sekund. En rekursiv funktion anropas, vilket push:ar ett flertal stack-frames på stacken.

Dessutom, registreras en timeout call-back, som efter ett visst antal sekunder stannar intervall timern. Efter att denna har exekverat är både stacken och kön tomma, vilket leder till att programmer avslutas.

```js
function factorial(n) {
    if (n < 0) return NaN;
    if (n === 0) return 0;
    if (n === 1) return 1;
    return n * factorial(n - 1);
}

function main(num_invocations = 5) {
    console.time('elapsed time');
    
    const timer = setInterval(() => {
        const argument = Math.floor(30 * Math.random());
        const result   = factorial(argument);
        console.log('%d! = %d', argument, result);
    }, 1000);
    
    setTimeout(() => {
        clearInterval(timer);
        console.timeEnd('elapsed time');
        }, (num_invocations + 1) * 1000);
}

main();
```

Så här kan det se ut när programmet körs:

```shell
5! = 120
23! = 2.585201673888498e+22
1! = 1
16! = 20922789888000
23! = 2.585201673888498e+22
elapsed time: 6.010s
```

Continuation-passing style
--------------------------

En call-back funktion som anropas kan anropa en annan funktion, som i sin tur tar en call-back och så vidare. Programexekveringen fortsätter i varje (nästlad) continuation, dvs call-back. Så här kan det se ut rent schematiskt:

```js
func1(args1, (result1) => {
    //...do something 1...
    func2(args2, (result2) => {
        //...do something 2...
        func3(arg3, (result3) => {
            //...do something 3...
        });
    });
});
```

Problemet med call-backs
------------------------

Ovanstående illustreras bäst med ett konkret exempel. Nedan visar jag ett program som kopierar innehållet i en fil (scriptet själv) till en ny fil (där texten gjorts om till versaler).

Funktionen `copyfile()` öppnar infilen för läsning, tar reda på hur stor den är och skapar en buffert för hela innehållet. Sedan läses innehållet in och filen stängs. Efter det öppnas utfilen för skrivning, varpå textinnehållet skrivs ut som versaler och filen stängs.

```js
import {close, fstat, open, read, write} from 'node:fs';
import {fileURLToPath} from 'node:url';
import {basename, dirname, extname, join} from 'node:path';

function die(err) { console.error(err); process.exit(1); }

function copyfile(fromFile, toFile) {
   open(fromFile, 'r', (err, fd) => {
      if (err) die(err);
      fstat(fd, (err, stats) => {
         if (err) die(err);
         const storage = Buffer.alloc(stats.size);
         read(fd, storage,0, storage.length,null, (err, bytesIn, data) => {
            if (err) die(err);
            console.log('Read  %d bytes from %s', bytesIn, basename(fromFile));
            close(fd, (err) => {
               if (err) die(err);
               open(toFile, 'w', (err, fd) => {
                  if (err) die(err);
                  const payload = data.toString().toUpperCase();
                  write(fd, payload, (err, bytesOut) => {
                     if (err) die(err);
                      close(fd, (err) => {
                        console.log('Wrote %d bytes to %s', bytesOut, basename(toFile));
                      });
                  });
               });
            });
         });
      })
   });
}

const scriptFilePath = fileURLToPath(import.meta.url);
const scriptFileName = basename(scriptFilePath, extname(scriptFilePath));
const outputFilePath = join(dirname(scriptFilePath), scriptFileName + '-copy.txt');

copyfile(scriptFilePath, outputFilePath);
```

Så här kan det se ut när programmet exekverar:

```shell
Read  1517 bytes from copyfile-callback.mjs
Wrote 1517 bytes to copyfile-callback-copy.txt
```

Exemplet ovan är förvisso ett väldigt litet exempel, men det visar ändock tydligt hur den indenterade programkoden glider mer och mer åt höger. Det här brukar kallas _bumerang-effekten_, eftersom koden liknar formen på en bumerang. Andra personer kallar det rätt och slätt _call-back hell_.

I nästa avsnitt, som kommer om en vecka, ska vi diskutera hur man fixar bumerang problemet med hjälp av löften om framtida värden, d.v.s. det som kallas _promises_.
