preventDefault
e stopPropagation
: quando utilizzare quale e cosa fa esattamente ciascun metodo.
Event.stopPropagation() ed Event.preventDefault()
La gestione degli eventi JavaScript è spesso semplice. Ciò vale soprattutto quando si ha a che fare con
una struttura HTML semplice (relativamente piatta). Le cose si complicano un po' quando gli eventi si muovono (o si propagano) attraverso una gerarchia di elementi. In genere, è quando gli sviluppatori contattano stopPropagation()
e/o preventDefault()
per risolvere i problemi che stanno riscontrando. Se
hai mai pensato: "Provvedo a provare preventDefault()
e se non funziona provo
stopPropagation()
e se non funziona provo entrambi", questo articolo fa per te. Ti spiegherò esattamente che cosa fa ogni metodo, quando utilizzarlo e ti fornirò una serie di esempi di lavoro da esplorare. Il mio obiettivo è porre fine a questa confusione una volta per tutte.
Prima di addentrarci troppo in dettaglio, però, è importante soffermarmi brevemente sui due tipi di gestione eventi possibili in JavaScript (in tutti i browser moderni, ossia Internet Explorer, prima della versione 9, non supportava l'acquisizione degli eventi).
Stili di eventi (acquisizione e propagazione)
Tutti i browser moderni supportano la cattura di eventi, ma viene utilizzata molto raramente dagli sviluppatori.
È interessante notare che era l'unica forma di eventi supportata originariamente da Netscape. Il rivale più grande di Netscape, Microsoft Internet Explorer, non supportava affatto la cattura di eventi, ma supportava solo un altro stile di eventi chiamato bubbling degli eventi. Quando è stato costituito il W3C, è stato riconosciuto il valore di entrambi gli stili di generazione di eventi e dichiarato che i browser devono supportarli entrambi, tramite un terzo parametro per il metodo addEventListener
. In origine, questo parametro era solo un booleano, ma tutti i browser moderni supportano un oggetto options
come terzo parametro, che puoi utilizzare per specificare, tra le altre cose, se vuoi utilizzare o meno la registrazione degli eventi:
someElement.addEventListener('click', myClickHandler, { capture: true | false });
Tieni presente che l'oggetto options
è facoltativo, così come la relativa proprietà capture
. Se uno dei due viene omesso, il
valore predefinito per capture
è false
, il che significa che verrà utilizzato il bubbling degli eventi.
Acquisizione di eventi
Che cosa significa se il gestore eventi è "in ascolto nella fase di acquisizione"? Per capirlo, dobbiamo sapere come si originano gli eventi e come si propagano. quanto segue vale per tutti gli eventi, anche se tu, in qualità di sviluppatore, non li utilizzi, non ti preoccupi o non ci pensi.
Tutti gli eventi iniziano nella finestra e passano prima attraverso la fase di acquisizione. Ciò significa che quando un evento viene inviato, inizia la finestra e si sposta "verso il basso" verso il suo elemento target prima. Ciò si verifica anche se stai ascoltando solo nella fase di formazione delle bolle. Considera il seguente markup di esempio e JavaScript:
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
Quando un utente fa clic sull'elemento #C
, viene inviato un evento che ha origine in window
. Questo evento
si propaga quindi attraverso i suoi discendenti nel seguente modo:
window
=> document
=> <html>
=> <body>
=> e così via, fino al raggiungimento del target.
Non importa se non è attivo alcun ascolto di un evento di clic nell'elemento window
o document
o
<html>
o <body>
(o in qualsiasi altro elemento sulla strada per il target). Un evento si verifica ancora nel window
e inizia il suo percorso come appena descritto.
Nel nostro esempio, l'evento di clic verrà propagato (questa è una parola importante perché si collega direttamente al funzionamento del metodo stopPropagation()
e verrà spiegata più avanti in questo documento) da window
al suo elemento target (in questo caso #C
) tramite tutti gli elementi tra window
e #C
.
Ciò significa che l'evento di clic inizierà alle ore window
e il browser porrà le seguenti
domande:
"C'è qualcosa in ascolto di un evento di clic su window
durante la fase di acquisizione?" In questo caso, verranno attivati i gestori di eventi appropriati. Nel nostro esempio non è così, quindi non verranno attivati gestori.
Successivamente, l'evento si propaga a document
e il browser chiede: "C'è qualcosa che ascolta
per un evento di clic su document
nella fase di acquisizione?" In questo caso, verranno attivati gli appropriati gestori eventi.
Successivamente, l'evento si propaga all'elemento <html>
e il browser chiede: "C'è qualcosa che ascolta un clic sull'elemento <html>
nella fase di acquisizione?" In questo caso, verranno attivati gli gestori di eventi appropriati.
Successivamente, l'evento si propaga all'elemento <body>
e il browser chiede: "C'è qualcosa che ascolta un evento di clic sull'elemento <body>
nella fase di acquisizione?" In questo caso, verranno attivati i gestori di eventi appropriati.
Successivamente, l'evento verrà propagato all'elemento #A
. Anche in questo caso, il browser chiederà: "Qualcosa è in ascolto per un evento di clic su #A
nella fase di acquisizione? Se sì, verranno attivati gli appropriati gestori eventi.
Successivamente, l'evento verrà propagato all'elemento #B
(e verrà posta la stessa domanda).
Infine, l'evento raggiungerà il target e il browser chiederà: "C'è qualcosa in ascolto di un
evento di clic sull'elemento #C
nella fase di acquisizione?". Questa volta la risposta è "sì". Questo breve periodo di tempo in cui l'evento si trova presente nel target è noto come "fase target". A questo punto, verrà attivato il gestore eventi, il browser console.log "#C è stato fatto clic" e abbiamo finito, giusto?
Sbagliato! Non abbiamo finito. Il processo continua, ma ora passa alla fase di bubbling.
Bollicine dell'evento
Il browser chiederà:
"C'è qualcosa che ascolta un evento di clic su #C
nella fase di bubbling?" Presta molta attenzione.
È del tutto possibile ascoltare i clic (o qualsiasi tipo di evento) sia nella fase di acquisizione sia
nella fase di bubbling. Se hai collegato i gestori eventi in entrambe le fasi (ad es. chiamando
.addEventListener()
due volte, una volta con capture = true
e una volta con capture = false
), allora sì,
entrambi i gestori eventi verranno attivati per lo stesso elemento. È però importante notare che vengono attivati in fasi diverse (una nella fase di acquisizione e una nella fase di bubbling).
Successivamente, l'evento si propaga (più comunemente definito "bolle" perché sembra che l'evento stia "risalendo" la struttura ad albero DOM) al suo elemento principale, #B
, e il browser chiederà: "Esiste un elemento che ascolta gli eventi di clic su #B
nella fase di propagazione?" Nel nostro esempio non c'è nulla,
quindi nessun gestore si attiverà.
Successivamente, l'evento mostrerà il fumetto #A
e il browser chiederà: "C'è qualcosa in ascolto di eventi di clic su #A
nella fase di bubbling?".
Successivamente, l'evento verrà sottoposto a bubbling a <body>
: "Qualcosa sta ascoltando gli eventi di clic sull'elemento <body>
nella fase di bubbling?"
Passiamo all'elemento <html>
: "È presente un elemento che ascolta gli eventi di clic sull'elemento <html>
nella fase di bubbling?
A questo punto, document
: "Esiste un elemento che ascolta gli eventi di clic su document
nella fase di bubbling?"
Infine, window
: "È presente un elemento che ascolta gli eventi di clic nella finestra nella fase di propagazione?"
Finalmente. È stato un lungo viaggio e il nostro evento è probabilmente molto stanco, ma, che ci crediate o meno, è il percorso che ogni evento deve affrontare. Il più delle volte questo non viene mai notato, perché gli sviluppatori sono in genere interessati solo a una fase dell'evento o all'altra (di solito è la fase di bubbling).
Vale la pena dedicare un po' di tempo a sperimentare l'acquisizione e la propagazione degli eventi e a registrare alcune note nella console quando vengono attivati gli handler. È molto utile vedere il percorso seguito da un evento. Ecco un esempio che ascolta ogni elemento in entrambe le fasi.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
L'output della console dipende dall'elemento su cui fai clic. Se fai clic sull'elemento "più in basso"
nella struttura DOM (l'elemento #C
), vedrai che viene attivato ogni singolo gestore di eventi. Con un po' di stile CSS per distinguere meglio gli elementi, ecco l'elemento #C
di output della console (con anche uno screenshot):
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
Puoi provare questa funzionalità in modo interattivo nella demo dal vivo di seguito. Fai clic sull'elemento #C
e osserva l'output della console.
event.stopPropagation()
Ora che sappiamo da dove provengono gli eventi e come si propagano (ovvero si diffondono) nel DOM sia nella fase di acquisizione sia in quella di propagazione, possiamo concentrarci su
event.stopPropagation()
.
Il metodo stopPropagation()
può essere chiamato per la maggior parte degli eventi DOM nativi. Dico "la maggior parte" perché ne esistono alcuni su cui l'uso di questo metodo non fa nulla (perché l'evento non si propaga inizialmente). Eventi come focus
, blur
, load
, scroll
e altri rientrano in questa
categoria. Puoi chiamare stopPropagation()
, ma non succederà nulla di interessante, poiché questi eventi
non si propagano.
Ma che cosa fa stopPropagation
?
Fa praticamente quello che dice. Quando lo chiami, l'evento, da quel momento, smette
di propagarsi a qualsiasi elemento a cui verrebbe altrimenti indirizzato. Questo vale per entrambe le direzioni
(acquisizione e propagazione). Pertanto, se chiami stopPropagation()
in qualsiasi punto della fase di acquisizione,
l'evento non arriverà mai alla fase di destinazione o di bubbling. Se lo chiami nella fase di effervescenza, avrà già superato la fase di acquisizione, ma smetterà di "risorgere" dal punto in cui lo hai chiamato.
Tornando allo stesso markup di esempio, cosa pensi che succederebbe se chiamassimo
stopPropagation()
nella fase di acquisizione nell'elemento #B
?
Il risultato sarà il seguente:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
Puoi provare questa funzionalità in modo interattivo nella demo dal vivo di seguito. Fai clic sull'elemento #C
nella demo dal vivo e osserva l'output della console.
Che ne dici di interrompere la propagazione a #A
nella fase di formazione di bolle? Il risultato potrebbe essere il seguente:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
Puoi provare questa funzionalità in modo interattivo nella demo dal vivo di seguito. Fai clic sull'elemento #C
nella demo dal vivo e osserva l'output della console.
Un'altra, giusto per divertimento. Cosa succede se chiamiamo stopPropagation()
nella fase target per #C
?
Ricorda che la "fase target" è il nome dato al periodo di tempo in cui l'evento è in corrispondenza del suo obiettivo. Il risultato sarà il seguente:
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
Tieni presente che il gestore eventi per #C
in cui registriamo "clic su #C nella fase di acquisizione" si esegue
ancora, ma non quello in cui registriamo "clic su #C nella fase di propagazione". Dovrebbe essere tutto chiaro. Abbiamo chiamato stopPropagation()
da il primo, quindi questo è il punto in cui la propagazione dell'evento cesserà.
Puoi provare questa funzionalità in modo interattivo nella demo dal vivo di seguito. Fai clic sull'elemento #C
nella demo dal vivo e osserva l'output della console.
Ti invitiamo a provare qualsiasi demo in tempo reale. Prova a fare clic solo sull'elemento #A
o solo sull'elemento body
. Prova a prevedere cosa accadrà e poi osserva se hai ragione. A questo punto, dovresti essere in grado di fare previsioni abbastanza precise.
event.stopImmediatePropagation()
Qual è questo metodo strano e poco utilizzato? È simile a stopPropagation
, ma anziché impedire a un evento di passare ai discendenti (acquisizione) o agli antenati (rifesteggiamento), questo metodo si applica solo quando hai più di un gestore eventi collegato a un singolo elemento. Poiché
addEventListener()
supporta uno stile di eventi multicast, è del tutto possibile collegare un
event handler a un singolo elemento più di una volta. In questo caso, (nella maggior parte dei browser), gli gestori degli eventi vengono eseguiti nell'ordine in cui sono stati collegati. La chiamata a stopImmediatePropagation()
impedisce l'attivazione di eventuali gestori successivi. Considera l'esempio seguente:
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
L'esempio riportato sopra produrrà il seguente output della console:
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
Tieni presente che il terzo gestore eventi non viene mai eseguito perché il secondo gestore eventi chiama
e.stopImmediatePropagation()
. Se avessimo chiamato e.stopPropagation()
, il terzo gestore sarebbe comunque stato eseguito.
event.preventDefault()
Se stopPropagation()
impedisce a un evento di passare "verso il basso" (acquisizione) o "verso l'alto" (rifermento), cosa fa preventDefault()
? Sembra che faccia qualcosa di simile. Lo fa?
Non esattamente. Sebbene i due termini vengano spesso confusi, in realtà non hanno molto a che fare l'uno con l'altro.
Quando vedi preventDefault()
, aggiungi mentalmente la parola "azione". Pensa a "impedire l'azione predefinita".
E qual è l'azione predefinita che potresti chiedere? Purtroppo, la risposta non è così chiara perché dipende fortemente dalla combinazione elemento + evento in questione. Per rendere le cose ancora più confuse, a volte non c'è del tutto alcuna azione predefinita.
Iniziamo con un esempio molto semplice da capire. Che cosa vi aspettate che succeda quando
fate clic su un link in una pagina web? Ovviamente, il browser dovrebbe accedere all'URL specificato da tale link.
In questo caso, l'elemento è un tag di ancoraggio e l'evento è un evento di clic. Questa combinazione (<a>
+
click
) ha un'"azione predefinita" che consiste nel passare all'attributo href del link. E se volessi impedire al browser di eseguire questa azione predefinita? Supponiamo che tu voglia impedire al browser di accedere all'URL specificato dall'attributo href
dell'elemento <a>
. Ecco cosa farà preventDefault()
per te. Considera questo esempio:
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
Puoi provare questa funzionalità in modo interattivo nella demo dal vivo di seguito. Fai clic sul link The Avett Brothers e osserva l'output della console (e il fatto che non venga visualizzato il sito web di The Avett Brothers).
In genere, facendo clic sul link The Avett Brothers si apre la paginawww.theavettbrothers.com
. In questo caso, però, abbiamo collegato un gestore dell'evento di clic all'elemento <a>
e abbiamo specificato che l'azione predefinita deve essere impedita. Pertanto, quando un utente fa clic su questo
link, non verrà reindirizzato a nessuna pagina, ma la console registrerà semplicemente "Forse dovremmo
riprodurre un po' della sua musica qui?"
Quali altre combinazioni di elementi/eventi ti consentono di impedire l'azione predefinita? Non posso elencarli tutti e a volte devi solo fare esperimenti per vedere. Eccone alcuni in breve:
Elemento
<form>
+ evento "submit":preventDefault()
per questa combinazione impedirà l'invio di un modulo. Questa operazione è utile se vuoi eseguire la convalida e, in caso di errore, puoi chiamare preventDefault in modo condizionale per interrompere l'invio del modulo.Elemento
<a>
+ evento "click":preventDefault()
per questa combinazione impedisce al browser di accedere all'URL specificato nell'attributo href dell'elemento<a>
.document
+ evento "mousewheel":preventDefault()
per questa combinazione impedisce lo scorrimento della pagina con la rotellina del mouse (lo scorrimento con la tastiera continuerà a funzionare).
↜ Per farlo, è necessario chiamareaddEventListener()
con{ passive: false }
.document
+ evento "keydown":preventDefault()
per questa combinazione è letale. La pagina diventa in gran parte inutilizzabile, impedendo lo scorrimento, il passaggio da una scheda all'altra e l'evidenziazione della tastiera.document
+ evento "mousedown":preventDefault()
per questa combinazione impedisce l'evidenziazione del testo con il mouse e qualsiasi altra azione "predefinita" che si potrebbe invocare con il mouse premuto.Elemento
<input>
+ evento "keypress":preventDefault()
per questa combinazione impedisce ai caratteri digitati dall'utente di raggiungere l'elemento di input (ma non farlo perché raramente, se non mai, esiste un motivo valido).document
+ evento "contextmenu":preventDefault()
per questa combinazione impedisce la visualizzazione del menu contestuale del browser nativo quando un utente fa clic con il tasto destro del mouse o preme a lungo (o in qualsiasi altro modo in cui potrebbe essere visualizzato un menu contestuale).
Non si tratta di un elenco esaustivo, ma speriamo che ti dia una buona idea di come può essere utilizzato preventDefault()
.
Una barzelletta pratica e divertente?
Cosa succede se stopPropagation()
e preventDefault()
nella fase di acquisizione, a partire dal documento? Risate assicurate! Il seguente snippet di codice renderà qualsiasi pagina web quasi completamente inutilizzabile:
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
Non so perché tu voglia fare una cosa del genere (tranne forse per scherzare con qualcuno), ma è utile pensare a cosa sta succedendo qui e capire perché crea la situazione.
Tutti gli eventi hanno origine da window
, quindi in questo snippet interrompiamo completamente tutti gli eventi click
, keydown
, mousedown
, contextmenu
e mousewheel
affinché non raggiungano mai gli elementi che potrebbero ascoltarli. Chiamiamo anche stopImmediatePropagation
in modo che anche eventuali gestori collegati al documento dopo questo vengano sventati.
Tieni presente che stopPropagation()
e stopImmediatePropagation()
non sono (almeno non per lo più) ciò che
rende la pagina inutile. Impediscono semplicemente che gli eventi vadano dove andare.
Tuttavia, chiamiamo anche preventDefault()
, che ricorderai impedisce l'azione predefinita. Pertanto, tutte le azioni predefinite (come scorrimento della rotellina del mouse, scorrimento della tastiera o evidenziazione o tabulazione, clic sui link, visualizzazione del menu contestuale e così via) vengono impedite, lasciando la pagina in uno stato abbastanza inutile.
Demo dal vivo
Per esplorare di nuovo tutti gli esempi di questo articolo in un unico posto, guarda la demo incorporata di seguito.
Ringraziamenti
Immagine hero di Tom Wilson su Unsplash.