Den dolda nyckeln till dynamiska webbinteraktioner

By rik

I min tidiga inlärningsfas inom webbutveckling stötte jag på något fascinerande och lite överraskande: händelsebubbling. Det kändes först ovant, men ju mer man tänker på hur händelser ”bubblar” desto mer logiskt framstår det. Som webbutvecklare kommer du med säkerhet att konfronteras med detta fenomen. Så, vad är egentligen händelsebubbling?

JavaScript använder händelser för att skapa interaktion mellan användare och webbsidor. En händelse refererar till aktiviteter eller åtgärder som din kod kan upptäcka och hantera. Exempel inkluderar musklick, tangenttryckningar och formulärinlämningar.

För att upptäcka och svara på dessa händelser använder JavaScript så kallade händelselyssnare. En händelselyssnare är i grunden en funktion som väntar på att en specifik händelse ska inträffa, till exempel ett klick på en knapp. När en händelselyssnare upptäcker den händelse den ”lyssnar” på, svarar den genom att köra den kod som är kopplad till den händelsen. Hela processen att fånga och svara på händelser kallas för händelsehantering.

Tänk dig nu en situation där vi har tre element på en sida: en div, en span och en knapp. Knappen är placerad inuti span-elementet, och span-elementet i sin tur inuti div-elementet. En illustration av detta ser du nedan:

Om vi antar att vart och ett av dessa element har en händelselyssnare som reagerar på ett klick och loggar ett meddelande i konsolen, vad händer när man klickar på knappen?

För att testa detta själv kan du skapa en mapp med tre filer: en HTML-fil (index.html), en CSS-fil (style.css) och en JavaScript-fil (app.js).

Lägg till följande kod i HTML-filen:

    <!DOCTYPE html>
    <html lang="sv">
    <head>
      <title>Händelsebubbling</title>
      <link rel="stylesheet" href="https://wilku.top/the-hidden-key-to-dynamic-web-interactions/style.css">
    </head>
    <body>
      <div>
        <span><button>Klicka här!</button></span>
      </div>
      <script src="app.js"></script>
    </body>
    </html>
  

I CSS-filen, lägg till följande för att styla div- och span-elementen:

    div {
      border: 2px solid black;
      background-color: orange;
      padding: 30px;
      width: 400px;
    }

    span {
      display: inline-block;
      background-color: cyan;
      height: 100px;
      width: 200px;
      margin: 10px;
      padding: 20px;
      border: 2px solid black;
    }
  

I JavaScript-filen, lägg till den här koden som kopplar händelselyssnare till div-, span- och knappelementen. Alla lyssnar efter ett klick:

    const div = document.querySelector('div');
    div.addEventListener('click', () => {
      console.log("Du klickade på div-elementet");
    })

    const span = document.querySelector('span');
    span.addEventListener('click', () => {
      console.log("Du klickade på span-elementet");
    })

    const button = document.querySelector('button');
    button.addEventListener('click', () => {
      console.log("Du klickade på knappen");
    })
  

Öppna nu HTML-filen i din webbläsare. Inspektera sidan och klicka sedan på knappen. Vad händer? Resultatet av att klicka på knappen ser du nedan:

Genom att klicka på knappen utlöses inte bara händelselyssnaren på knappen. Utan även de händelselyssnare som är kopplade till span- och div-elementen. Varför sker detta?

När du klickar på knappen aktiveras händelselyssnaren som hör till den, vilket resulterar i utskriften i konsolen. Men eftersom knappen ligger inuti ett span-element, innebär ett klick på knappen tekniskt sett också ett klick på span-elementet, och dess händelselyssnare aktiveras därmed.

Eftersom span-elementet i sin tur är placerat inuti div-elementet, kommer ett klick på span-elementet även att registreras som ett klick på div-elementet. Därför utlöses även dess händelselyssnare. Detta är det som kallas händelsebubbling.

Händelsebubbling

Händelsebubbling är en process där en händelse som startar i ett kapslat HTML-element ”bubblar” uppåt i DOM-trädet. Den börjar i det innersta elementet där händelsen inträffade och fortsätter upp till rotelementet, vilket aktiverar alla händelselyssnare som väntar på just den typen av händelse.

Händelselyssnarna triggas i en specifik ordning som motsvarar hur händelsen bubblar upp genom DOM-trädet. Tänk på DOM-trädet nedan, som visar strukturen på HTML-koden vi använde tidigare:

En klickhändelse som bubblar upp genom DOM-trädet

DOM-trädet visar hur knappen ligger inuti span, som ligger inuti div, i sin tur inuti body och slutligen inuti HTML-elementet. När du klickar på knappen kommer klickhändelsen först att utlösa händelselyssnaren som är kopplad direkt till knappen.

Men eftersom elementen är kapslade kommer händelsen därefter att ”bubbla” uppåt i DOM-trädet, från knappen till span, sedan till div, body och till sist HTML-elementet. Under den här processen aktiveras alla händelselyssnare som lyssnar efter ett klick.

Det är därför händelselyssnarna kopplade till span- och div-elementen också utförs. Om det fanns händelselyssnare på body- och HTML-elementen skulle de också triggas.

Det element i DOM-trädet där händelsen sker, kallas för målet (target). I vårt fall, eftersom klicket sker på knappen, är knappen målet.

Hur man stoppar händelsebubbling

För att stoppa en händelse från att ”bubbla” upp genom DOM använder vi metoden `stopPropagation()`, som finns tillgänglig i händelseobjektet. Ta en titt på kodexemplet nedan, där vi lägger till en händelselyssnare på knappen:

    const button = document.querySelector('button');
    button.addEventListener('click', () => {
      console.log("Du klickade på knappen");
    })
  

Denna kod leder till att händelsen bubblar upp genom DOM när en användare klickar på knappen. För att förhindra det anropar vi metoden `stopPropagation()` enligt nedan:

    const button = document.querySelector('button');
    button.addEventListener('click', (e) => {
      console.log("Du klickade på knappen");
      e.stopPropagation();
    })
  

En händelsehanterare är den funktion som körs när en knapp klickas. En händelselyssnare skickar automatiskt ett händelseobjekt till händelsehanteraren. I vårt fall representeras det händelseobjektet av variabeln `e`, som skickas som parameter till händelsehanteraren.

Detta objekt `e` innehåller information om händelsen och ger oss tillgång till många olika egenskaper och metoder relaterade till händelser, bland annat metoden `stopPropagation()`, som vi använder för att förhindra händelsebubbling. Genom att anropa `stopPropagation()` i knappens händelselyssnare stoppar vi händelsen från att bubbla upp i DOM-trädet från knappen.

Resultatet av att klicka på knappen efter att ha lagt till `stopPropagation()` visas nedan:

Vi använder alltså `stopPropagation()` för att förhindra att en händelse bubblar upp från det element där vi använder metoden. Om vi, till exempel, ville att klickhändelsen skulle ”bubbla” upp från knappen till span-elementet, men inte längre, skulle vi använda `stopPropagation()` på span-elementets händelselyssnare.

Händelsefångst

Händelsefångst är det motsatta till händelsebubbling. Med händelsefångst ”sipprar” en händelse nedåt från det yttersta elementet till det element som är målet, vilket illustreras nedan:

Klickhändelsen ”sipprar” nedåt till målet via händelsefångst

Till exempel, i vårt fall när du klickar på knappen så kommer, vid händelsefångst, händelselyssnarna på div-elementet att vara de första som triggas. Därefter följer lyssnarna på span-elementet och sist lyssnarna på målet, knappen.

Händelsebubbling är dock det standardmässiga sättet som händelser sprids i DOM. För att ändra detta beteende och använda händelsefångst i stället, skickar vi ett tredje argument till våra händelselyssnare som sätter händelsefångst till sant. Om inget tredje argument ges, är händelsefångst automatiskt falskt.

Titta på den här händelselyssnaren:

    div.addEventListener('click', () => {
      console.log("Du klickade på div-elementet");
    })
  

Eftersom den saknar ett tredje argument är fångst inställt på falskt. För att ändra det till sant lägger vi till det booleska värdet `true` som ett tredje argument:

    div.addEventListener('click', () => {
      console.log("Du klickade på div-elementet");
    }, true)
  

Alternativt kan vi skicka ett objekt med `capture` satt till `true`:

    div.addEventListener('click', () => {
      console.log("Du klickade på div-elementet");
    }, {capture: true})
  

För att testa händelsefångst, lägg till ett tredje argument till alla dina händelselyssnare i din JavaScript-fil:

    const div = document.querySelector('div');
    div.addEventListener('click', () => {
      console.log("Du klickade på div-elementet");
    }, true)

    const span = document.querySelector('span');
    span.addEventListener('click', (e) => {
      console.log("Du klickade på span-elementet");
    }, true)

    const button = document.querySelector('button');
    button.addEventListener('click', () => {
      console.log("Du klickade på knappen");
    }, true)
  

Öppna webbläsaren och klicka på knappen. Du bör få en utskrift som ser ut så här:

Lägg märke till att i motsats till händelsebubbling, där knappen skrevs ut först, så är den första utskriften nu från det yttersta elementet, div.

Händelsebubbling och händelsefångst är de två huvudsakliga metoderna för hur händelser sprids i DOM, men händelsebubbling är det vanligaste sättet.

Händelsedelegation

Händelsedelegation är ett designmönster där en enskild händelselyssnare kopplas till ett gemensamt föräldraelement, som till exempel ett `<ul>`-element, i stället för att ha händelselyssnare på vart och ett av de underliggande elementen. Händelser på barn-elementen ”bubblar” upp till föräldraelementet, där de hanteras av händelsehanteraren på föräldraelementet.

Det innebär att om vi har ett föräldraelement med underliggande element, så lägger vi bara en händelselyssnare på föräldraelementet. Denna händelsehanterare kommer att ta hand om alla händelser som sker i barn-elementen.

Du kanske undrar hur föräldraelementet kan veta vilket barn-element som klickades på. Som nämnts tidigare, skickar en händelselyssnare ett händelseobjekt till händelsehanteraren. Det objektet har metoder och egenskaper som ger information om den specifika händelsen. En av egenskaperna i händelseobjektet är `target`-egenskapen. `Target`-egenskapen pekar på det HTML-element där händelsen faktiskt inträffade.

Om vi till exempel har en oordnad lista med listelement och vi kopplar en händelselyssnare till `<ul>`-elementet, kommer `target`-egenskapen i händelseobjektet att peka på det specifika `<li>`-elementet där händelsen inträffade.

För att se händelsedelegation i praktiken, lägg till följande HTML-kod till din befintliga HTML-fil:

    <ul>
        <li>Toyota</li>
        <li>Subaru</li>
        <li>Honda</li>
        <li>Hyundai</li>
        <li>Chevrolet</li>
        <li>Kia</li>
      </ul>
  

Lägg nu till följande JavaScript-kod för att använda händelsedelegation och hantera händelser i barn-elementen med en enda händelselyssnare på föräldraelementet:

    const ul = document.querySelector('ul');
    ul.addEventListener('click', (e) => {
      // elementet som klickades på
      targetElement = e.target
      // skriv ut innehållet i elementet
      console.log(targetElement.textContent)
    })
  

Öppna webbläsaren och klicka på valfritt element i listan. Innehållet i det elementet ska nu skrivas ut i konsolen:

Med en enda händelselyssnare kan vi nu alltså hantera händelser i alla underliggande element. Om man använder många händelselyssnare på en sida kan det påverka prestandan negativt, öka minnesanvändningen och leda till att sidor laddas och renderas långsammare.

Händelsedelegation hjälper oss att undvika dessa problem genom att minimera antalet händelselyssnare som vi behöver använda på en sida. Händelsedelegation bygger på händelsebubbling, så man kan säga att händelsebubbling också kan bidra till att optimera prestandan på en webbsida.

Tips för effektiv händelsehantering

Som utvecklare, när du jobbar med händelser i DOM, är det bra att försöka använda händelsedelegation snarare än att ha många händelselyssnare på element på din sida.

När du använder händelsedelegation, tänk på att koppla händelselyssnaren till den närmaste gemensamma förfadern för de barn-element som behöver händelsehantering. Detta optimerar händelsebubbling och minimerar den sträcka som en händelse måste ”resa” innan den hanteras.

När du hanterar händelser, använd händelseobjektet som händelselyssnaren tillhandahåller till din fördel. Det innehåller egenskaper som `target`, som är mycket användbara för att hantera händelser.

För att skapa snabbare webbplatser, undvik att ändra DOM i onödan. Att ha händelser som frekvent manipulerar DOM kan påverka webbplatsens prestanda negativt.

Slutligen, om du jobbar med kapslade element, var mycket försiktig med att lägga till händelselyssnare. Det kan påverka prestandan negativt och göra händelsehanteringen komplex och din kod svår att underhålla.

Slutsats

Händelser är ett kraftfullt verktyg i JavaScript. Händelsebubbling, händelsefångst och händelsedelegation är viktiga att förstå för att hantera händelser i JavaScript. Som webbutvecklare kan du använda den här artikeln för att lära dig grunderna och skapa mer interaktiva, dynamiska och snabba webbplatser och applikationer.