Uma pergunta simples: Se um elemento e um de seus ancestrais tiverem ambos listeners definidos para o mesmo evento, qual deles deverá ser disparado primeiro? Não surpreendentemente, depende do navegador.
<div id="um">
<div id="dois">
<div id="tres">Rá!</div>
</div>
</div>
No trecho HTML acima temos um elemento #tres
dentro de um elemento #dois
, que por sua vez é filho do elemento #um
. Agora definiremos manipuladores para o evento click em dois destes elementos:
document.getElementById( 'tres' ).addEventListener( 'click', function( event ){
console.log( '#tres clicado' )
})
document.getElementById( 'um' ).addEventListener( 'click', function( event ){
console.log( '#um clicado' )
})
// ou, jQuery like
$('#tres').click( function( event ){
console.log( '#tres clicado' )
})
$('#um').click( function( event ){
console.log( '#um clicado' )
})
Agora, quando clicarmos em #tres
, o que você acha que irá acontecer? Dependendo do navegador, a ordem das execuções dos eventos pode ser invertida, poderemos ter primeiro “#tres clicado” ou “#um clicado”. Vamos entender o porquê desta bagunça.
Netscape vs. Internet Explorer
Como tudo na vida, há sempre um background histórico. E no caso de hoje, tudo começou lá na época da guerra dos navegadores (se você ainda não assistiu, por favor assista!), quando os únicos navegadores do mercado (Netscape e IE) implementavam suas features da maneira que bem entendiam.
Não surpreendentemente…
Netscape e Microsoft chegaram a diferentes conclusões sobre como a propagação de eventos deveria ocorrer.
Na abordagem da Netscape, o click de #um
teria precedência e seria disparado primeiro – chamaram isso de fase de captura (ou simplesmente capturing).
Já na abordagem da Microsoft, o click de #tres
teria precendência e seria disparado primeiro – chamaram isso de fase de borbulhamentobubbling (ou simplesmente bubbling), justamente pelo fato de o evento se propagar em direção a raiz do documento, como se estivesse “subindo a árvore DOM”, ou seja, como se estivesse “borbulhando” para cima.
Capturing ou bubbling?
As duas abordagens são completamente opostas. Porém, em ambas é feita uma varredura em todos os elementos ancestrais ao elemento de onde ocorreu o evento. A diferença está justamente na ordem de como esta varredura será feita.
Fase de captura
Na captura, os elementos ancestrais têm precedência na varredura, de modo que a varredura ocorra sempre partindo do elemento mais básico em direção até o elemento mais específico (onde ocorreu o evento). No nosso exemplo, a fase de captura ocorreria nesta ordem:
- Caso o usuário clique em
#tres
:#um
»#dois
»#tres
. - Caso o usuário clique em
#dois
:#um
»#dois
.
Fase de bubbling
No bubbling, os elementos específicos têm precedência na varredura, de modo que a varredura ocorra sempre partindo do elemento mais específico até o elemento mais básico (em direção à raiz do documento). No nosso exemplo, a fase de bubbling ocorreria nesta ordem:
- Caso o usuário clique em
#tres
:#tres
»#dois
»#um
. - Caso o usuário clique em
#dois
:#dois
»#um
.
Lembrando que ainda há outros elementos mais básicos do documento, que também sofrem as varreduras:
O exemplo acima ilustra o caso de o usuário clicar em #tres
.
Abaixo temos um exemplo interativo. Clique em cada um dos discos e veja uma simulação do que acontece na realidade. A mudança de cor de cada elemento representa a varredura por eventos no mesmo.
A solução W3C: capturing com bubbling
Na hora de padronizar, a W3C optou pela implementação de ambos os modelos. Então, em um navegador que segue o padrão atual, qualquer evento primeiramente passa pela fase de captura – até chegar ao elemento onde ocorreu o evento corrente (target) – e então depois pela fase de bubbling.
A escolha é nossa
Então quer dizer que agora todos os eventos disparam 2 vezes (uma vez na fase de captura e outra vez na fase de bubbling)? Não.
Nós, desenvolvedores, podemos escolher se queremos registrar um manipulador de eventos (handler) na fase de captura ou bubbling. Isto é possível com o método para registro de eventos .addEventListener()
– que faz parte da DOM Level 2 Event specification da W3C. .addEventListener()
possui 3 parâmetros:
type
: tipo de eventohandler
: função manipuladora do eventouseCapture
: setrue
, registra o manipulador na fase de captura, caso contrário, registra na fase de bubbling. Este parâmetro é opcional e o seu valor padrão éfalse
, ou seja, registros de eventos são feitos na fase de bubbling por padrão.
If true,
useCapture
indicates that the user wishes to initiate capture. After initiating capture, all events of the specified type will be dispatched to the registeredEventListener
before being dispatched to anyEventTargets
beneath them in the tree. Events which are bubbling upward through the tree will not trigger anEventListener
designated to use capture.
Testando
Abaixo temos exemplos para ambos os tipos de registros:
// registrando 'click' do elemento #tres na fase de bubbling (padrão)
document.getElementById( 'tres' ).addEventListener( 'click', function( event ){
console.log( '#tres clicado' )
})
// registrando 'click' do elemento #dois na fase de capturing
document.getElementById( 'dois' ).addEventListener( 'click', function( event ){
console.log( '#dois clicado' )
}, true)
Eis o que acontecerá (na ordem) quando o usuário clicar em #tres
:
- Começa a varredura da fase de captura – iniciando pelo elemento mais ancestral;
- A varredura de captura não encontra nada no elemento
#um
para captura; - A varredura de captura encontra registro do evento no elemento
#dois
para captura e então o dispara (o console imprime “#dois clicado”); - A varredura de captura chega ao elemento target (
#tres
) – onde ocorreu o clique – e não encontra nada para captura. Termina a varredura da fase de captura; - Começa a varredura da fase de bubbling – iniciando pelo elemento onde ocorreu o clique (
#tres
); - A varredura de bubbling encontra registro do evento no elemento
#tres
para bubbling e então o dispara (o console imprime “#tres clicado”); - A varredura de bubbling não encontra nada no elemento
#dois
para bubbling; - A varredura de bubbling não encontra nada no elemento
#um
para bubbling; - A varredura de bubbling prossegue até a raiz do documento e termina.
Abaixo, clique em #tres
e veja o que acontece visualmente. Depois que a interação acabar, tente clicar em #dois
.
E se…
…adicionarmos um manipulador para o evento click
em cada uma das fases para o mesmo elemento? O que acontecerá?
// registrando 'click' do elemento #tres na fase de bubbling
document.getElementById( 'tres' ).addEventListener( 'click', function( event ){
console.log( '#tres clicado' )
})
// registrando O MESMO 'click' do MESMO ELEMENTO na fase de capturing
document.getElementById( 'tres' ).addEventListener( 'click', function( event ){
console.log( '#tres clicado' )
}, true)
Felizmente, isto não é possível. O que vai acontecer no trecho de código acima é a sobrescrita do novo registro sobre o último. Então o manipulador registrado na fase de captura permanecerá, enquanto o outro (bubbling) será descartado.
Cancelando a propagação? Mamata!
Consideremos o seguinte cenário: Digamos que temos os elementos #um
, #dois
e #tres
– todos com manipuladores para o evento click na fase de bubbling. Agora, digamos que, por alguma razão, quando ocorrer um clique em #tres
, os outros cliques em #dois
e #tres
não deverão ser disparados.
document.getElementById( 'tres' ).addEventListener( 'click', function( event ){
console.log( '#tres clicado' )
})
document.getElementById( 'dois' ).addEventListener( 'click', function( event ){
console.log( '#dois clicado' )
})
document.getElementById( 'um' ).addEventListener( 'click', function( event ){
console.log( '#um clicado' )
})
Pelo código acima, quando o usuário clicar em #tres
, a saída do console será:
#tres clicado
#dois clicado
#um clicado
Mas queremos apenas isto:
#tres clicado
O que queremos na verdade é impedir que o evento seja propagado para o resto dos elementos. Ou seja, parar com a varredura da fase de bubbling a partir de um determinado momento. E para isso usaremos o método .stopPropagation()
, que pertence ao objeto do próprio evento:
document.getElementById( 'tres' ).addEventListener( 'click', function( event ){
console.log( '#tres clicado' )
event.stopPropagation()
})
E voilá! O bubbling parou por aí.
Bem, então voltando a pergunta do início deste post: Se um elemento e um de seus ancestrais tiverem ambos listeners definidos para o mesmo evento, qual deles deverá ser disparado primeiro?
No IE8 e inferiores, o elemento corrente (onde ocorreu o clique) disparará primeiro, e depois seus ancestrais. Já nos navegadores de respeito (IE9 incluso), vai depender de como os manipuladores foram registrados.