preventDefault
e stopPropagation
: quando utilizzare l'uno o l'altro e cosa fa esattamente ciascun metodo.
Event.stopPropagation() e Event.preventDefault()
La gestione degli eventi JavaScript è spesso semplice. Ciò è particolarmente vero quando si ha a che fare con una
struttura HTML semplice (relativamente piatta). Le cose si fanno un po' più complicate quando gli eventi
si spostano (o si propagano) attraverso una gerarchia di elementi. In genere, gli sviluppatori si rivolgono a stopPropagation()
e/o preventDefault()
per risolvere i problemi che riscontrano. Se
ti è mai capitato di pensare "Proverò preventDefault()
e se non funziona proverò
stopPropagation()
e se non funziona proverò entrambi", allora questo articolo fa al caso tuo. Ti spiegherò esattamente cosa fa ogni metodo, quando utilizzarlo e ti fornirò una serie di esempi funzionanti da esplorare. Il mio obiettivo è porre fine alla tua confusione una volta per tutte.
Prima di entrare 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 l'acquisizione degli eventi).
Stili di gestione degli eventi (acquisizione e bubbling)
Tutti i browser moderni supportano l'acquisizione degli eventi, ma viene utilizzata molto raramente dagli sviluppatori.
È interessante notare che era l'unica forma di gestione degli eventi originariamente supportata da Netscape. Il principale rivale di Netscape, Microsoft Internet Explorer, non supportava affatto l'acquisizione di eventi, ma solo un altro stile di gestione degli eventi chiamato bubbling degli eventi. Quando è stato creato il W3C, è stato ritenuto opportuno
supportare entrambi gli stili di gestione degli eventi ed è stato dichiarato che i browser avrebbero dovuto supportarli entrambi tramite un terzo parametro del
metodo addEventListener
. In origine, questo parametro era solo un valore 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 l'acquisizione degli eventi o meno:
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
Cosa significa se il gestore eventi è "in ascolto nella fase di acquisizione"? Per capire questo, dobbiamo sapere come hanno origine gli eventi e come si propagano. Quanto segue vale per tutti gli eventi, anche se tu, in qualità di sviluppatore, non lo utilizzi, non ti interessa o non lo prendi in considerazione.
Tutti gli eventi iniziano dalla finestra e passano prima alla fase di acquisizione. Ciò significa che quando viene inviato un evento, questo avvia la finestra e si sposta "verso il basso" verso l'elemento di destinazione prima. Ciò si verifica anche se stai solo ascoltando durante la fase di bubbling. Considera il seguente markup ed esempio 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 propagherà quindi ai relativi discendenti come segue:
window
=> document
=> <html>
=> <body>
=> e così via, fino a raggiungere il target.
Non importa se non viene rilevato alcun evento di clic nell'elemento window
, document
, <html>
o <body>
(o in qualsiasi altro elemento nel percorso verso la destinazione). Un evento ha comunque
origine in window
e inizia il suo percorso come descritto.
Nel nostro esempio, l'evento di clic verrà quindi propagato (questa è una parola importante perché si collegherà
direttamente al funzionamento del metodo stopPropagation()
, che verrà spiegato più avanti in questo documento)
da window
al relativo elemento di destinazione (in questo caso, #C
) tramite ogni elemento tra
window
e #C
.
Ciò significa che l'evento di clic inizierà alle ore window
e il browser porrà le seguenti
domande:
"Qualcuno sta ascoltando un evento di clic su window
nella fase di acquisizione?" In questo caso, verranno attivati i gestori di eventi appropriati. Nel nostro esempio, non è presente alcun evento, quindi non verrà attivato alcun gestore.
Successivamente, l'evento si propaga a document
e il browser chiede: "Qualcuno sta ascoltando
un evento di clic su document
nella fase di acquisizione?" In questo caso, verranno attivati i gestori di eventi appropriati.
Successivamente, l'evento si propaga all'elemento <html>
e il browser chiede: "Qualcuno
sta ascoltando un clic sull'elemento <html>
nella fase di acquisizione?" In questo caso, verranno attivati i gestori
di eventi appropriati.
Successivamente, l'evento verrà propagato all'elemento <body>
e il browser chiederà: "Qualcuno
sta ascoltando 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à: "Qualcuno
è in ascolto di un evento di clic su #A
nella fase di acquisizione e, in caso affermativo, verranno attivati i gestori di eventi
appropriati.
Successivamente, l'evento verrà propagato all'elemento #B
(e verrà posta la stessa domanda).
Infine, l'evento raggiungerà il target e il browser chiederà: "Qualcuno è in ascolto di un
evento di clic sull'elemento #C
nella fase di acquisizione?" La risposta questa volta è "sì". Questo breve
periodo di tempo in cui l'evento è sul target è noto come "fase target". A questo punto, il
gestore eventi verrà attivato, il browser eseguirà console.log "#C was clicked" e poi avremo finito, giusto?
Sbagliato. Non abbiamo ancora finito. Il processo continua, ma ora passa alla fase di bubbling.
Propagazione degli eventi
Il browser chiederà:
"Qualcuno sta ascoltando un evento di clic su #C
nella fase di bubbling?" Presta molta attenzione.
È assolutamente possibile ascoltare i clic (o qualsiasi tipo di evento) sia nella fase di acquisizione che in quella di bubbling. Se hai collegato i gestori di eventi in entrambe le fasi (ad es. chiamando
.addEventListener()
due volte, una con capture = true
e una con capture = false
), allora sì,
entrambi i gestori di eventi verranno attivati per lo stesso elemento. Tuttavia, è anche importante notare che
vengono attivati in fasi diverse (uno nella fase di acquisizione e uno nella fase di bubbling).
Successivamente, l'evento si propaga (più comunemente indicato come "bubble" perché sembra che l'evento si sposti "verso l'alto" nell'albero DOM) al relativo elemento padre, #B
, e il browser chiederà: "Qualcuno sta ascoltando gli eventi di clic su #B
nella fase di bubbling?" Nel nostro esempio, non è presente nulla, quindi
non verrà attivato alcun gestore.
Successivamente, l'evento verrà propagato a #A
e il browser chiederà: "Qualcuno sta ascoltando gli eventi di clic
su #A
nella fase di bubbling?"
Successivamente, l'evento verrà propagato a <body>
: "Qualcuno sta ascoltando gli eventi di clic sull'elemento <body>
nella fase di bubbling?"
Poi, l'elemento <html>
: "Qualcuno sta ascoltando gli eventi di clic sull'elemento <html>
nella
fase di bubbling?
Poi, document
: "Qualcuno sta ascoltando gli eventi di clic su document
nella fase di bubbling?"
Infine, window
: "Qualcuno sta ascoltando gli eventi di clic nella finestra durante la fase di bubbling?"
Finalmente. È stato un lungo viaggio e il nostro evento è probabilmente molto stanco ormai, ma che tu ci creda o no, questo è il percorso che ogni evento deve affrontare. La maggior parte delle volte, questo non viene mai notato perché gli sviluppatori sono in genere interessati solo a una fase dell'evento o all'altra (e di solito è la fase di bubbling).
Vale la pena dedicare un po' di tempo a sperimentare l'acquisizione, la propagazione e la registrazione degli eventi e annotare alcune note nella console quando vengono attivati i gestori. È molto utile vedere il percorso che segue 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ù profondo"
nell'albero DOM (l'elemento #C
), vedrai attivarsi
ognuno di questi gestori di eventi. Con un po' di stile CSS per rendere più evidente quale elemento è quale, ecco l'output della console
dell'elemento #C
(con 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"
event.stopPropagation()
Ora che abbiamo capito da dove provengono gli eventi e come si spostano (ovvero si propagano) nel DOM
sia nella fase di acquisizione sia in quella di bubbling, possiamo concentrarci su
event.stopPropagation()
.
Il metodo stopPropagation()
può essere chiamato sulla maggior parte degli eventi DOM nativi. Dico "la maggior parte" perché
su alcune chiamare questo metodo non fa nulla (perché l'evento non si propaga
in primo luogo). Eventi come focus
, blur
, load
, scroll
e altri rientrano in questa
categoria. Puoi chiamare stopPropagation()
, ma non succederà nulla di interessante, perché questi eventi
non vengono propagati.
Ma che cosa fa stopPropagation
?
Fa esattamente quello che dice. Quando lo chiami, l'evento smetterà
di propagarsi a tutti gli elementi a cui altrimenti si propagherebbe. Questo vale per entrambe le direzioni
(acquisizione e bubbling). Quindi, se chiami stopPropagation()
in qualsiasi punto della fase di acquisizione, l'evento non raggiungerà mai la fase di destinazione o di bubbling. Se lo chiami nella fase di bubbling, avrà già superato la fase di acquisizione, ma smetterà di "salire" dal punto in cui lo hai chiamato.
Tornando allo stesso markup di esempio, cosa pensi che succederebbe se chiamassimo
stopPropagation()
nella fase di acquisizione dell'elemento #B
?
L'output 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"
Che ne dici di interrompere la propagazione a #A
nella fase di bubbling? In questo modo, otterrai il seguente
output:
"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"
Un altro, giusto per divertimento. Cosa succede se chiamiamo stopPropagation()
nella fase di targeting per #C
?
Ricorda che la "fase target" è il nome dato al periodo di tempo in cui l'evento è al suo
target. L'output 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 "click su #C nella fase di acquisizione" viene
ancora eseguito, ma quello in cui registriamo "click su #C nella fase di bubbling" no. Dovrebbe essere
tutto chiaro. Abbiamo chiamato stopPropagation()
da il primo, quindi questo è il punto in cui la
propagazione dell'evento cesserà.
Ti invito a provare le demo online. Prova a fare clic solo sull'elemento #A
o
solo sull'elemento body
. Prova a prevedere cosa succederà e poi osserva se la tua previsione è corretta. A
questo punto, dovresti essere in grado di fare previsioni piuttosto accurate.
event.stopImmediatePropagation()
Qual è questo metodo strano e poco utilizzato? È simile a stopPropagation
, ma anziché
impedire a un evento di raggiungere i discendenti (acquisizione) o gli antenati (propagazione), questo metodo
si applica solo quando hai più di un gestore di eventi collegato a un singolo elemento. Poiché
addEventListener()
supporta uno stile di eventi multicast, è assolutamente possibile collegare un
gestore di eventi a un singolo elemento più di una volta. Quando ciò accade, (nella maggior parte dei browser), i gestori di eventi vengono eseguiti nell'ordine in cui sono stati collegati. La chiamata 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 precedente genererà 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 invece avessimo chiamato e.stopPropagation()
, il terzo gestore
sarebbe comunque stato eseguito.
event.preventDefault()
Se stopPropagation()
impedisce a un evento di spostarsi "verso il basso" (acquisizione) o "verso l'alto"
(propagazione), cosa fa preventDefault()
? Sembra che faccia qualcosa di simile. Does
it?
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 molto dalla combinazione di elemento ed evento in questione. E per rendere le cose ancora più confuse, a volte non esiste alcuna azione predefinita.
Iniziamo con un esempio molto semplice per capire meglio. Cosa ti aspetti che succeda quando fai clic su un link in una pagina web? Ovviamente, ti aspetti che il browser vada 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'href del link. Cosa succederebbe se volessi impedire
al browser di eseguire l'azione predefinita? Supponiamo, ad esempio, di voler impedire al browser
di passare all'URL specificato dall'attributo href
dell'elemento <a>
. Questo è ciò che
preventDefault()
farà 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,
);
Normalmente, se fai clic sul link con l'etichetta The Avett Brothers, si apre la pagina www.theavettbrothers.com
. In questo caso, tuttavia, abbiamo collegato un gestore di eventi 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à indirizzato da nessuna parte e 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 elencarle tutte e a volte devi solo sperimentare per vedere. Ecco alcuni esempi:
L'elemento
<form>
+ l'evento "submit":preventDefault()
per questa combinazione impedirà l'invio di un modulo. Ciò è utile se vuoi eseguire la convalida e, in caso di errore, puoi chiamare preventDefault in modo condizionale per impedire l'invio del modulo.Elemento
<a>
+ evento "click":preventDefault()
per questa combinazione impedisce al browser di passare all'URL specificato nell'attributo href dell'elemento<a>
.document
+ evento "rotellina del mouse":preventDefault()
questa combinazione impedisce lo scorrimento della pagina con la rotellina del mouse (lo scorrimento con la tastiera funziona comunque).
↜ Questa operazione richiede la chiamata diaddEventListener()
con{ passive: false }
.document
+ evento "keydown":preventDefault()
per questa combinazione è letale. La pagina diventa in gran parte inutilizzabile, impedendo lo scorrimento, la navigazione tramite tasto Tab e l'evidenziazione tramite tastiera.document
+ evento "mousedown":preventDefault()
per questa combinazione impedirà l'evidenziazione del testo con il mouse e qualsiasi altra azione "predefinita" che si invocherebbe con un clic del mouse.Elemento
<input>
+ evento "keypress":preventDefault()
per questa combinazione impedirà ai caratteri digitati dall'utente di raggiungere l'elemento di input (ma non farlo; raramente, se non mai, c'è un motivo valido per farlo).document
+ evento "contextmenu":preventDefault()
questa combinazione impedisce la visualizzazione del menu contestuale nativo del browser quando un utente fa clic con il tasto destro del mouse o preme a lungo (o in qualsiasi altro modo in cui potrebbe apparire un menu contestuale).
Questo non è un elenco esaustivo, ma spero che ti dia una buona idea di come
preventDefault()
può essere utilizzato.
Uno scherzo divertente?
Cosa succede se stopPropagation()
e preventDefault()
nella fase di acquisizione, a partire dal
documento? Il divertimento è assicurato! Il seguente snippet di codice renderà qualsiasi pagina web
quasi completamente inutile:
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 farlo (forse per fare uno scherzo a qualcuno), ma è utile capire cosa succede e perché si crea questa situazione.
Tutti gli eventi hanno origine in window
, quindi in questo snippet stiamo bloccando sul nascere tutti gli eventi click
, keydown
, mousedown
, contextmenu
e mousewheel
prima che raggiungano gli elementi che potrebbero ascoltarli. Chiamiamo anche stopImmediatePropagation
in modo che anche gli
handler collegati al documento dopo questo vengano bloccati.
Tieni presente che stopPropagation()
e stopImmediatePropagation()
non sono (almeno non per la maggior parte) ciò che
rende inutile la pagina. Impediscono semplicemente agli eventi di raggiungere la destinazione prevista.
Ma chiamiamo anche preventDefault()
, che, come ricorderai, impedisce l'azione predefinita. Pertanto, qualsiasi
azione predefinita (come lo scorrimento della rotellina del mouse, lo scorrimento della tastiera o l'evidenziazione o la selezione con il tasto Tab, il clic sui link, la visualizzazione del menu contestuale e così via) viene impedita, lasciando la pagina in uno stato piuttosto inutile.
Ringraziamenti
Immagine promozionale di Tom Wilson su Unsplash.