Meir fart og mindre snork med CustomEvent i JavaScript

I dette innlegget så presenterer eg programmerings-mønster for polling og eventar i JavaScript, og argumenterer kvifor eventar kan vere å foretrekkje.

Blå bok med teksten fra boller til burritos

Knipsa eit foto av denne perla på BIR sin bruktmarknad i Bergen.

Om ein skal skrive interaktive applikasjonar i nettlesaren så må ein gi seg i kast med asynkron programmering i JavaScript (JS). I praksis tyder det at dersom du skriv fem kodelinjer i JS så kan du ikkje nødvendigvis forvente at desse kodelinjene vil bli køyrd i pen og pynteleg (synkron) orden. Om ein av desse kodelinjene til dømes gjer er eit kall for å hente data frå ei ekstern teneste så vil JS gjerne suse vidare og eksekvere resten av koden din uten å vente på svar frå den eksterne tenesta.

Det kjem altså litt an på (iik).

Heldigvis har utviklarar jobba fram betre byggjeklosser og mønster (patterns) for effektiv asynkron programmering.

La oss sjå litt nærare på polling og bruk av eventar.

Å polle seg til mål #

Polling går ut på å rigge koden din slik at den kontinuerleg spør etter det den treng for å kome vidare.

La oss sei at du har ein knapp som skal fyre opp ein satellitt og kontakte denne, men den er ikkje klar med ein gong. Difor skriv me noko setInterval() kode som kontinuerleg pollar etter om satellitten er klar til å svare. Me tyr også til Promises + async await for å få koden vår til å køyre i riktig rekkjefølge og vente der den bør.

<script>
// Ein stad langt uti galaksen (eller i djupet av fjerne moduler) så fyres det av ein
// immediately invoked function expression som tilgjengeleggjer fasiliteter for
// oppskyting av satellittar.
// 
// Meir om IIFE: https://developer.mozilla.org/en-US/docs/Glossary/IIFE
(() => {
    const skytOpp = () => {
        console.log('Skyter opp ...')
        setTimeout(() => {
            // Etter noko tid finst det ein satellitt der ute.
            globalThis.satellitt = {
                ping() { return 'beep boop' },
                destruer() { globalThis.satellitt = null }
            }
            console.log('Satellitt er ferdig skutt opp')
        }, 5000)
    }
    globalThis.skytOpp = skytOpp
})()

function finnSatellitt() {
    if (globalThis.satellitt) {
        return Promise.resolve(globalThis.satellitt)
    }
    return new Promise((resolve, reject) => {
        let teller = 0
        const intervalID = setInterval(() => {
            teller++
            console.log('Er satellitten klar? Sjekk nr:', teller)
            if (teller > 10) {
                clearInterval(intervalID)
                reject(new Error('fant ikke satellitt'))
                return
            }
            if (globalThis.satellitt) {
                clearInterval(intervalID)
                resolve(globalThis.satellitt)
                return
            }
        }, 1000)
    })
}

document
    .querySelector('#js-aktiver')
    .addEventListener('click', async function (event) {
        const knapp = event.target
        // Unngå dobbeltklikking mens me venter
        knapp.disabled = true
        const originalTekst = knapp.innerText
        knapp.innerText = 'skyter opp ...'

        globalThis.skytOpp()
    
        try {
            const satellitt = await finnSatellitt()
            console.log('Svar frå satellitt:', satellitt.ping())
            satellitt.destruer()
        } catch (err) {
            console.log('Woops! noko gjekk galt:', err)
        } finally {
            knapp.innerText = originalTekst
            knapp.disabled = false              
            console.log('Denne log-linjen printes til slutt.')
        }
    })

</script>

<button id="js-aktiver">skyt opp satellitt</button>

Om du vil køyre koden i nettlesaren så har eg også lagt den ut her: Codepen: Døme på JavaScript polling.

Koden over ser kanskje ikkje så ukjend ut? Når utviklarar skal hanskast med asynkron programmering så tyr mange til polling for å løyse oppgåvene sine. Det fungerer godt til mange formål.

Men ei ulempe ved polling er at det kan vere vanskeleg å finne riktig balanse på kor ofte skal koden polle? Skal den spørje kvart sekund dersom det tek 3 sekundar å bli klar?

Kva om koden av ulike grunnar tek 5 sekund å bli klar og du har satt intervallet til å sjekke kvart 4 sekund? Då må brukaren potensielt vente i 8 sekund før dei får respons. Kva om brukaren har raskt nett eller ein rask maskin og koden er klar etter 0.1 sekund, skal brukaren likevel vente 4 sekund?

Skal me då sitje timesvis å prøve å kode oss fram til nokre intervall-tal som representerer eit “minst-verst optimum” for brukarane våre? Nei la oss heller sjekke om å bruke eventer kan vere eit betre alternativ til polling.

Gi meg ein lyd når du er klar #

Denne koden er nesten heilt lik den forrige, men her tek me i bruk CustomEvent som eit alternativ til polling.

<script>
// Nok ein gong er me ein stad langt uti galaksen (eller i djupet av fjerne moduler) 
(() => {
    const skytOpp = () => {
        console.log("Skyter opp ...");
        setTimeout(() => {
            // Etter noko tid finst det ein satellitt der ute.
            globalThis.satellitt = {
                ping() { return "beep boop" },
                destruer() { globalThis.satellitt = null; }
            };
            console.log("Satellitt er ferdig skutt opp");
            globalThis.dispatchEvent(new CustomEvent('satellitt-er-klar'))
        }, 5000);
    };
    globalThis.skytOpp = skytOpp;
})();

function finnSatellitt() {
    if (globalThis.satellitt) {
        return Promise.resolve(globalThis.satellitt);
    }
    return new Promise((resolve, reject) => {
        const timeoutID = setTimeout(() => {
            reject(new Error("fant ikke satellitt"));
        }, 8000)
        function lytter() {
            clearTimeout(timeoutID)
            globalThis.removeEventListener('satellitt-er-klar', lytter)
            resolve(globalThis.satellitt)
        }
        globalThis.addEventListener('satellitt-er-klar', lytter)
    });
}

document
    .querySelector("#js-aktiver")
    .addEventListener("click", async function (event) {
        const knapp = event.target;
        // Unngå dobbeltklikking mens me venter
        knapp.disabled = true;
        const originalTekst = knapp.innerText;
        knapp.innerText = "skyter opp ...";

        globalThis.skytOpp();

        try {
            const satellitt = await finnSatellitt();
            console.log("Svar frå satellitt:", satellitt.ping());
            satellitt.destruer();
        } catch (err) {
            console.log("Woops! noko gjekk galt:", err);
        } finally {
            knapp.innerText = originalTekst;
            knapp.disabled = false;
            console.log("Denne log-linjen printes til slutt.");
        }
    });
</script>

<button id="js-aktiver">skyt opp satellitt</button>

Denne koden er også tilgjengeleg her, Codepen: Satellittoppskyting med eventer.

Eg vil ikkje hevde at denne koden er mykje enklare enn polling, fordi det er ikkje færre kodelinjer og ein må fortsatt tenkje på god feilhandtering.

Likevel så har denne event-baserte koden ein tydeleg fordel framfor polling ved at den kan gå så raskt som mogleg, og den skuslar ikkje vekk tida til PCen eller brukaren på å spørje i tide og utide om programmet er klar til å gå vidare.

Eg håper dette mønsteret kan vere eit nyttig tilskudd til verktøykassa di. Men pass på at det ikkje blir ein gyllen hammar du ukritisk bruker overalt.

Eg har tidvis hatt behov for å få ulik JS-kode som «ligg langt unna» kvarandre i frontend til å samhandle og der det har vore vanskeleg å opprette direkte kontakt. Då har dette mønsteret vist seg å vere effektivt.

Resten av isberget #

Det er mykje anna eg kunne (eller burde) nemne om asynkron JS. Det blir diverre for langt for dette innlegget, men her er nokre konsepter som det kan vere kjekt å utforske vidare.