preventDefault
e stopPropagation
: quando utilizzarli e che cosa fa esattamente ciascun metodo.
Event.stopPropagation() ed Event.preventDefault()
La gestione degli eventi JavaScript è spesso semplice. Questo è particolarmente vero 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 è chiarire definitivamente i tuoi dubbi.
Prima di addentrarci troppo nel dettaglio, è importante accennare brevemente ai due tipi di gestione degli eventi possibili in JavaScript (in tutti i browser moderni, ovvero Internet Explorer prima della versione 9 non supportava affatto il rilevamento 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 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 dalla finestra e passano prima dalla fase di acquisizione. Ciò significa che quando viene inviato un evento, viene avviata la finestra e si sposta "verso il basso" verso l'elemento di destinazione prima. Ciò si verifica anche se stai ascoltando solo nella fase di formazione di bolle. Considera il seguente markup e JavaScript di esempio:
<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 proveniente da window
. Questo evento
si propagherà poi ai suoi discendenti come segue:
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 ogni elemento tra window
e #C
.
Ciò significa che l'evento di clic inizierà a window
e il browser farà le seguenti domande:
"C'è qualcosa che ascolta un evento di clic su window
nella 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 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 suo target e il browser chiederà: "C'è qualcosa che ascolta 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 è in corrispondenza del 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 ancora finito. Il processo continua, ma ora passa alla fase di bubbling.
Event bubbling
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 nelle fasi 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 è così, quindi non verranno attivati gestori.
Successivamente, l'evento verrà sottoposto a bubbling a #A
e il browser chiederà: "C'è qualcosa che ascolta gli 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 in genere sono interessati solo a una fase dell'evento o all'altra (in genere è 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'attivazione 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 in poi, non si propagherà più agli elementi a cui altrimenti si propagherebbe. 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 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"
"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" viene
ancora eseguito, 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 succederà e poi osserva se hai ragione. A questo punto, dovresti essere in grado di fare previsioni abbastanza precise.
event.stopImmediatePropagation()
Che cos'è 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 di eventi non viene mai eseguito perché il secondo gestore di 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 stopPropagation()
?preventDefault()
Sembra che faccia qualcosa di simile.
Sì?
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 è molto chiara perché dipende molto dalla combinazione di elementi ed eventi in questione. Per complicare ulteriormente le cose, a volte non esiste alcuna azione predefinita.
Iniziamo con un esempio molto semplice da comprendere. Che cosa ti aspetti che accada quando fai clic su un link su una pagina web? Ovviamente, ti aspetti che il browser acceda all'URL specificato dal 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:
Elemento
<form>
+ evento "submit":preventDefault()
per questa combinazione impedisce 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 immissione (ma non farlo; raramente, se non mai, esiste un motivo valido per farlo).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()
.
Uno scherzo 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 davvero perché tu voglia farlo (tranne che per fare uno scherzo a qualcuno), ma è utile pensare a cosa sta succedendo e capire perché si crea questa 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 la maggior parte) ciò che
rende la pagina inutilizzabile. Semplicemente impediscono agli eventi di arrivare dove altrimenti andrebbero.
Chiamiamo anche preventDefault()
, che come 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, dai un'occhiata alla demo incorporata di seguito.
Ringraziamenti
Immagine hero di Tom Wilson su Unsplash.