Il ciclo di vita di una pagina HTML è costituito da 3 importanti eventi:
DOMContentLoaded
– il browser ha completamente caricato l’HTML, e l’albero del DOM è stato costruito, ma risorse esterne come immagini<img>
e i fogli di stile potrebbero ancora non essere stati caricati.load
– non solo l’HTML è caricato ma anche tutte le risorse esterne: immagini, fogli di stile, ecc.beforeunload/unload
– l’utente sta lasciando la pagina.
Ogni evento potrebbe essere utile:
- evento di
DOMContentLoaded
quando il DOM è pronto, quindi il gestore può scorrere i nodi del DOM, ed inizializzare l’interfaccia. - evento di
load
quando tutte le risorse esterne sono state caricate, ed anche gli stili sono stati applicati, le dimensioni delle immagini nella pagina è nota, ecc. - evento di
beforeunload
quando l’utente sta lasciando la pagina: possiamo controllare se l’utente ha salvato le modifiche effettuate ed eventualmente chiedergli se è sicuro di voler lasciare la pagina. - evento di
unload
quando l’utente ha quasi lasciato la pagina ma possiamo ancora effettuare alcune operazioni.
Esploriamo i dettagli di questi eventi.
DOMContentLoaded
L’evento DOMContentLoaded
viene emesso dall’oggetto document
.
Dobbiamo quindi utilizzare addEventListener
per catturarlo:
document.addEventListener("DOMContentLoaded", ready);
// non "document.onDOMContentLoaded = ..."
Per esempio:
<script>
function ready() {
alert('DOM is ready');
// l'immagine non è stata ancora caricata (a meno che non fosse già presente in cache) quindi la dimensione è 0x0
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
}
document.addEventListener("DOMContentLoaded", ready);
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
Nell’esempio il gestore DOMContentLoaded
si aziona quando il documento è caricato, quindi può vedere tutti gli elementi del DOM, compresa <img>
sotto.
Ma non aspetta che l’immagine si carichi. Per questo alert
mostra zero come dimensione.
Ad una prima occhiata l’evento DOMContentLoaded
è molto semplice. Ci segnala che l’albero del DOM è pronto, tutto qui. Ci però sono alcune peculiarità.
DOMContentLoaded e gli script
Quando il browser processa un documento HTML ed incontra un tag <script>
deve eseguirlo prima di continuare a costruire il DOM. Questa è una precauzione, visto che gli script potrebbero voler modificare il DOM, e ci potrebbero anche essere dei document.write
all’interno, quindi DOMContentLoaded
deve aspettare.
Quindi l’evento DOMContentLoaded avviene sicuramente dopo questi script:
<script>
document.addEventListener("DOMContentLoaded", () => {
alert("DOM pronto!");
});
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
<script>
alert("Libreria caricata, script inline eseguito");
</script>
Nell’esempio sopra, prima vediamo “Libreria caricata…”, e poi “DOM pronto!” (tutti gli script sono stati eseguiti).
Ci sono 2 eccezioni per questa regola:
- Gli script con l’attributo
async
, che vedremo successivamente (info:script-async-defer), non bloccanoDOMContentLoaded
. - Gli script generati dinamicamente con
document.createElement('script')
e poi aggiunti alla pagina non bloccano questo evento.
DOMContentLoaded e stili
I fogli di stile esterni non influenzano il caricamento del DOM, quindi DOMContentLoaded
non aspetta il loro caricamento.
Ma c’è una trappola. Se abbiamo uno script dopo uno stile quello script deve aspettare affinché il foglio di stile sia stato caricato.
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// lo script non viene eseguito finché il foglio di stile non è caricato
alert(getComputedStyle(document.body).marginTop);
</script>
La ragione è che lo script potrebbe voler ottenere le coordinate e altre proprietà dipendenti dallo stile degli elementi, come nell’esempio sopra. Di conseguenza lo script deve aspettare che gli stili siano caricati.
Quindi DOMContentLoaded
aspetta il caricamento degli script e inoltre anche quello degli stili.
Riempimento automatico nativo del browser
Firefox, Chrome e Opera riempiono automaticamente i form durante l’evento DOMContentLoaded
.
Per esempio, se la pagina contiene un form con username e password, e il browser ha memorizzato i valori, allora durante l’evento DOMContentLoaded
prova a riempire automaticamente i campi (se approvato dall’utente).
Se l’evento DOMContentLoaded
viene rinviato a causa dei lunghi tempi di caricamento degli script, allora anche il riempimento automatico dovrà attendere. Probabilmente avrete visto questo comportamento su alcuni siti (se utilizzate browser con riempimento automatico) – i campi della login/password non vengono riempiti immediatamente, ma c’è un delay(ritardo) finché la pagina non è completamente caricata. Questo è in realtà il ritardo dovuto all’evento DOMContentLoaded
.
window.onload
L’evento load
sull’oggetto window
viene emesso quando tutta la pagina è stata caricata, inclusi gli stili, immagini e altre risorse.
L’esempio sotto mostra correttamente le dimensioni dell’immagine, perché window.onload
aspetta il caricamento di tutte le immagini:
<script>
window.onload = function() { // equivale window.addEventListener('load', (event) => {
alert('Page loaded');
// l'immagine è già caricata in questo momento
alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
};
</script>
<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
window.onunload
Quando un utente lascia la pagina viene emesso l’evento unload
sulla window
. Possiamo effettuare operazioni che non comportano un ritardo, come chiudere le finestre popup relative alla pagina.
L’eccezione degna di nota è mandare dati analitici riguardo la pagina.
Diciamo che raccogliamo dati su come viene utilizzata la pagina: click del mouse, scroll, aree della pagina visitate e così via.
Naturalmente, l’evento unload
si verifica quando l’utente ci sta lasciando e a noi vorremmo salvare i dati sul nostro server.
Esiste un metodo speciale navigator.sendBeacon(url, data)
per questa necessità, descritto nei dettagli https://w3c.github.io/beacon/.
Manda i dati in background. La transizione tra una pagina e l’altra non viene ritardata: il browser lascia la pagina ma esegue comunque sendBeacon
.
Ecco come utilizzarlo:
let analyticsData = { /* oggetto con i dati raccolti */ };
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
- La richiesta è effettuata come POST.
- Non siamo limitati all’invio di sole stringhe, ma sono supportati anche
Form
ed altri formati, come descritto nel capitolo Fetch, anche se solitamente si invia un oggetto sotto forma di stringa. - La dimensione massima dei dati è 64kb.
Quando la richiesta sendBeacon
è terminata il browser probabilmente ha già lasciato il document e quindi non c’è modo di ottenere la risposta del server (che comunque di solito è vuota per i dati analitici).
C’è anche un flag keepalive
per effettuare richieste dopo che la pagina è stata lasciata. Potrai trovare maggiore informazioni nel capitolo API Fetch.
Se vogliamo cancellare il passaggio ad un’altra pagina non possiamo farlo qui ma dobbiamo utilizzare un altro evento – onbeforeunload
.
window.onbeforeunload
Se un utente sta cambiando pagina o sta cercando di chiudere la finestra, il gestore dell’evento beforeunload
chiede una conferma aggiuntiva.
Se cancelliamo l’evento il browser potrebbe chiedere all’utente se confermano l’operazione.
Puoi provare questo comportamento eseguendo questo codice e poi ricaricando la pagina:
window.onbeforeunload = function() {
return false;
};
Per ragioni storiche, anche tornare una stringa non vuota conta come cancellazione dell’evento. Poco tempo fa i browser lo mostravano come un messaggio ma come dicono le specifiche moderne, non dovrebbero.
Qui vediamo un esempio:
window.onbeforeunload = function() {
return "There are unsaved changes. Leave now?";
};
Il comportamento è stato cambiato perché alcuni webmaster abusavano di questo evento mostrando messaggi fastidiosi e fuorvianti. Quindi ora i vecchi browser potrebbero mostrare ancora questi messaggi, ma a parte questo – non c’è modo di personalizzare il messaggio che viene mostrato all’utente.
event.preventDefault()
doesn’t work from a beforeunload
handlerThat may sound weird, but most browsers ignore event.preventDefault()
.
Which means, following code may not work:
window.addEventListener("beforeunload", (event) => {
// doesn't work, so this event handler doesn't do anything
event.preventDefault();
});
Instead, in such handlers one should set event.returnValue
to a string to get the result similar to the code above:
window.addEventListener("beforeunload", (event) => {
// works, same as returning from window.onbeforeunload
event.returnValue = "There are unsaved changes. Leave now?";
});
readyState
Cosa succede se impostiamo il gestore dell’evento DOMContentLoaded
dopo che il documento è stato caricato?
Naturalmente, non verrà mai eseguito.
Ci sono dei casi in cui non siamo sicuri se il document è stato caricato o meno. Vorremmo che la nostra funzione venisse eseguita quando il DOM è stato caricato, sia adesso che dopo.
La proprietà document.readyState
ci da informazioni a proposito dello stato di caricamento corrente.
Può assumere 3 diversi valori:
"loading"
– il document si sta caricando."interactive"
– il document è stato completamente letto."complete"
– il document è stato completamente letto e anche tutte le risorse (come le immagini) sono state caricate.
Quindi possiamo controllare la proprietà document.readyState
e in base al suo valore eseguire codice o impostare un handler.
Come questo:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// sta ancora caricando, si attende l'evento
document.addEventListener('DOMContentLoaded', work);
} else {
// Il DOM è pronto
work();
}
Abbiamo a disposizione anche l’evento readystatechange
che viene emesso quando lo stato cambia, quindi possiamo stampare il cambiamento di questi stati, come nell’esempio:
// stato corrente
console.log(document.readyState);
// stampa cambi di stato
document.addEventListener('readystatechange', () => console.log(document.readyState));
L’evento readystatechange
è un meccanismo alternativo per il tracciamento dello stato di caricamento della pagina ed è comparso diverso tempo fa. Al giorno d’oggi viene usato raramente.
Vediamo il flusso di tutti gli eventi per completezza.
Ecco un documento con <iframe>
, <img>
e handler che mostra gli eventi:
<script>
log('initial readyState:' + document.readyState);
document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));
window.onload = () => log('window onload');
</script>
<iframe src="iframe.html" onload="log('iframe onload')"></iframe>
<img src="http://en.js.cx/clipart/train.gif" id="img">
<script>
img.onload = () => log('img onload');
</script>
L’esempio funzionante è nella sandbox.
L’output tipico:
- [1] initial readyState:loading
- [2] readyState:interactive
- [2] DOMContentLoaded
- [3] iframe onload
- [4] img onload
- [4] readyState:complete
- [4] window onload
I numeri tra le parentesi quadre indicano il tempo approssimativo di quando l’evento si verifica. Gli eventi etichettati con la stessa cifra avvengono allo stesso tempo circa (± qualche ms)
document.readyState
diventainteractive
appena prima diDOMContentLoaded
. Queste 2 cose in realtà significano la stessa cosa.document.readyState
diventacomplete
quando tutte le risorse (iframe
eimg
) sono caricate. Qui possiamo vedere che avviene all’incirca nello stesso tempo diimg.onload
(img
è l’ultima risorsa) ewindow.onload
. Passare allo statocomplete
significa lo stesso diwindow.onload
. La differenza è chewindow.onload
si attiva sempre dopo tutti gli altriload
handler.
Riepilogo
Eventi di caricamento della pagina:
- l’evento
DOMContentLoaded
scatta suldocument
quando il DOM è pronto. Possiamo quindi utilizzare Javascript sugli elementi della pagina in questa fase.- Script come
<script>...</script>
o<script src="..."></script>
bloccano DOMContentLoaded poiché il browser aspetta che finiscano di caricarsi per eseguirlo. - Immagini e altre risorse potrebbero prolungare ulteriormente il caricamento.
- Script come
- l’evento
load
sullawindow
si aziona quando la pagina e tutte le risorse sono caricate. Viene usato raramente perché di solito non c’è bisogno di attendere per così tanto. - l’evento
beforeunload
sullawindow
si aziona quando l’utente vuole lasciare la pagina. Se cancelliamo l’evento il browser chiede all’utente se vuole veramente lasciare la pagina (per esempio se ci sono modifiche non salvate). - l’evento
unload
sullawindow
si aziona quando l’utente sta proprio lasciando la pagina, nelil gestore possiamo solo effettuare operazioni semplici che non comportano nessun ritardo o chiedere cose all’utente. Vista questa limitazione, viene usato raramente. Possiamo mandare richieste di rete connavigator.sendBeacon
. document.readyState
è lo stato corrente del documento, i cambi possono essere tracciati nell’eventoreadystatechange
:loading
– il document si sta caricando.interactive
– il document è parsato, si verifica quasi allo stesso tempo diDOMContentLoaded
, ma prima di esso.complete
--il document e tutte le risorse sono caricate, si verifica quasi allo stesso tempo diwindow.onload
, ma prima di esso.