Detalhamento do evento JavaScript

preventDefault e stopPropagation: quando usar cada um e o que exatamente cada método faz.

O processamento de eventos do JavaScript geralmente é simples. Isso é especialmente verdadeiro ao lidar com uma estrutura HTML simples (relativamente plana). As coisas ficam um pouco mais complicadas quando os eventos estão viajando (ou se propagando) por uma hierarquia de elementos. Isso geralmente acontece quando os desenvolvedores entram em contato para receber stopPropagation() e/ou preventDefault() e resolver os problemas que estão enfrentando. Se você já pensou: "Vou tentar o preventDefault() e, se isso não funcionar, vou tentar stopPropagation() e se isso não funcionar, vou tentar os dois", este artigo é para você. Vou explicar exatamente o que cada método faz, quando usar cada um e fornecer vários exemplos de trabalho para você explorar. Meu objetivo é acabar com sua confusão de uma vez por todas.

Antes de nos aprofundarmos, é importante falar brevemente sobre os dois tipos de processamento de eventos possíveis no JavaScript (em todos os navegadores modernos, o Internet Explorer anterior à versão 9 não oferecia suporte à captura de eventos).

Estilos de eventos (captura e propagação)

Todos os navegadores modernos oferecem suporte à captura de eventos, mas isso raramente é usado pelos desenvolvedores. Curiosamente, essa foi a única forma de evento que o Netscape suportava originalmente. O maior rival do Netscape, o Microsoft Internet Explorer, não oferecia suporte à captura de eventos, mas sim a outro estilo de evento chamado de bubbling. Quando o W3C foi formado, ele encontrou mérito nos dois estilos de evento e declarou que os navegadores deveriam oferecer suporte a ambos, por meio de um terceiro parâmetro para o método addEventListener. Originalmente, esse parâmetro era apenas um booleano, mas todos os navegadores modernos oferecem suporte a um objeto options como o terceiro parâmetro, que pode ser usado para especificar (entre outras coisas) se você quer usar a captura de eventos ou não:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

O objeto options é opcional, assim como a propriedade capture. Se um deles for omitido, o valor padrão de capture será false, o que significa que o evento de bubbling será usado.

Captura de eventos

O que significa quando o gerenciador de eventos está "escutando na fase de captura"? Para entender isso, precisamos saber como os eventos se originam e como eles acontecem. O seguinte é verdadeiro para todos os eventos, mesmo que você, como desenvolvedor, não os utilize, se preocupe com eles ou pense neles.

Todos os eventos começam na janela e passam pela fase de captura. Isso significa que, quando um evento é enviado, ele inicia a janela e viaja "para baixo" em direção ao elemento de destino primeiro. Isso acontece mesmo se você estiver ouvindo apenas na fase de bubbling. Considere o seguinte exemplo de marcação 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 um usuário clica no elemento #C, um evento, originado no window, é enviado. Esse evento será propagado pelos descendentes da seguinte forma:

window => document => <html> => <body> => e assim por diante, até atingir a meta.

Não importa se nada está detectando um evento de clique no elemento window ou document ou <html> ou <body> (ou qualquer outro elemento no caminho para o destino). Um evento ainda se origina no window e começa a jornada conforme descrito.

No nosso exemplo, o evento de clique vai se propagar (essa é uma palavra importante, porque vai se conectar diretamente à forma como o método stopPropagation() funciona e será explicada mais adiante neste documento) do window para o elemento de destino (neste caso, #C) por todos os elementos entre o window e o #C.

Isso significa que o evento de clique vai começar em window e o navegador vai fazer as seguintes perguntas:

"Há algum evento de clique na window na fase de captura?" Nesse caso, os manipuladores de eventos apropriados serão disparados. No nosso exemplo, nada é, então nenhum manipulador é acionado.

Em seguida, o evento vai ser propagado para o document, e o navegador vai perguntar: "Há algo detectando um evento de clique no document na fase de captura?" Nesse caso, os manipuladores de eventos apropriados serão acionados.

Em seguida, o evento será propagado para o elemento <html> e o navegador perguntará: "Alguma coisa está detectando um clique no elemento <html> na fase de captura?" Se sim, os processadores de eventos adequados serão acionados.

Em seguida, o evento vai ser propagado para o elemento <body>, e o navegador vai perguntar: "Há algum listener de evento de clique no elemento <body> na fase de captura?" Nesse caso, os manipuladores de eventos adequados serão acionados.

Em seguida, o evento será propagado para o elemento #A. Novamente, o navegador vai perguntar: "Há algo aguardando um evento de clique em #A na fase de captura? Se sim, os manipuladores de eventos apropriados serão acionados.

Em seguida, o evento será propagado para o elemento #B, e a mesma pergunta será feita.

Por fim, o evento vai alcançar o destino e o navegador vai perguntar: "Há algo que esteja ouvindo um evento de clique no elemento #C na fase de captura?" Desta vez, a resposta é "sim". Esse breve período de tempo em que o evento está na meta é conhecido como "fase de destino". Neste ponto, o manipulador de eventos será acionado, o navegador console.log "#C foi clicado" e pronto. Errado! Não terminamos por isso. O processo continua, mas agora muda para a fase de bubbling.

Fluxo de eventos

O navegador perguntará:

"Há algo detectando um evento de clique em #C na fase de bubbling?" Preste muita atenção aqui. É possível detectar cliques (ou qualquer tipo de evento) nas duas fases de captura e de propagação. E se você tivesse conectado manipuladores de eventos em ambas as fases (por exemplo, chamando .addEventListener() duas vezes, uma com capture = true e outra com capture = false), os dois manipuladores de eventos seriam absolutamente disparados para o mesmo elemento. No entanto, também é importante observar que elas são disparadas em fases diferentes (uma na fase de captura e outra na fase de propagação).

Em seguida, o evento vai se propagar (mais comumente chamado de "bubble" porque parece que o evento está "subindo" na árvore DOM) para o elemento pai, #B, e o navegador vai perguntar: "Há alguma coisa detectando eventos de clique em #B na fase de bubbling?" Em nosso exemplo, nada é, então nenhum gerenciador será disparado.

Em seguida, o evento vai ser transmitido para #A, e o navegador vai perguntar: "Alguma coisa está detectando eventos de clique em #A na fase de transmissão?"

Em seguida, o evento vai ser transmitido para <body>: "Algo está detectando eventos de clique no elemento <body> na fase de transmissão?"

Em seguida, o elemento <html>: "Algo está detectando eventos de clique no elemento <html> na fase de bubbling?

Em seguida, o document: "Algo está detectando eventos de clique no document na fase de bolhamento?"

Por fim, o window: "Há algo detectando eventos de clique na janela na fase de bubbling?"

Ufa. Foi uma longa jornada, e nosso evento provavelmente está muito cansado, mas acredite, essa é a jornada que todo evento precisa passar! Na maioria das vezes, isso nunca é notado porque os desenvolvedores geralmente estão interessados apenas em uma fase de evento ou outra (e geralmente é a fase de bubbling).

Vale a pena passar um tempo brincando com a captura de eventos e o bubbling de eventos e registrar algumas notas no console quando os manipuladores são acionados. É muito esclarecedor analisar o caminho que um evento percorre. Confira um exemplo que detecta todos os elementos nas duas fases.

<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,
);

A saída do console vai depender do elemento em que você clicar. Se você clicar no elemento "mais profundo" na árvore DOM (o elemento #C), todos esses manipuladores de eventos serão acionados. Com um pouco de estilo CSS para deixar mais claro qual é o elemento qual, aqui está o elemento #C de saída do console (com uma captura de tela também):

"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"

Você pode brincar com ele de forma interativa na demonstração ao vivo abaixo. Clique no elemento #C e observe a saída do console.

event.stopPropagation()

Com uma compreensão de onde os eventos se originam e como eles viajam (ou seja, se propagam) pelo DOM nas fases de captura e de propagação, agora podemos voltar nossa atenção para event.stopPropagation().

O método stopPropagation() pode ser chamado na maioria dos eventos DOM nativos. Digo "a maioria" porque há algumas em que chamar esse método não gera nada (porque o evento não é propagado para começar). Eventos como focus, blur, load, scroll e alguns outros se enquadram nessa categoria. Você pode chamar stopPropagation(), mas nada interessante vai acontecer, já que esses eventos não se propagam.

Mas o que stopPropagation faz?

Ele faz exatamente o que diz. Quando você o chama, o evento deixa de se propagar para os elementos que ele alcançaria. Isso é válido para ambas as direções (captura e bolha). Portanto, se você chamar stopPropagation() em qualquer lugar da fase de captura, o evento nunca vai chegar à fase de destino ou de propagação. Se você chamá-lo na fase de bolhas, ele já terá passado pela fase de captura, mas deixará de "surgir" a partir do ponto em que você o chamou.

Voltando à mesma marcação de exemplo, o que você acha que aconteceria se chamasse stopPropagation() na fase de captura no elemento #B?

O resultado seria o seguinte:

"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"

Você pode interagir com isso na demonstração ao vivo abaixo. Clique no elemento #C na demonstração ao vivo e observe a saída do console.

Que tal interromper a propagação em #A na fase de propagação? Isso resultaria na seguinte saída:

"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"

Você pode interagir com isso na demonstração ao vivo abaixo. Clique no elemento #C na demonstração ao vivo e observe a saída do console.

Mais uma, só por diversão. O que acontece se chamarmos stopPropagation() na fase de destino para #C? A "fase de destino" é o nome dado ao período em que o evento está no destino. O resultado seria o seguinte:

"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"

O gerenciador de eventos para #C em que registramos "clique em #C na fase de captura" ainda é executado, mas o que registramos "clique em #C na fase de expansão" não é. Isso faz todo o sentido. Chamamos stopPropagation() de o primeiro, então esse é o ponto em que a propagação do evento vai cessar.

Você pode brincar com ele de forma interativa na demonstração ao vivo abaixo. Clique no elemento #C na demonstração ao vivo e observe a saída do console.

Em qualquer uma dessas demonstrações ao vivo, recomendamos que você brinque. Tente clicar apenas no elemento #A ou no elemento body. Tente prever o que vai acontecer e observe se você está certo. Nesse ponto, você será capaz de fazer previsões com bastante precisão.

event.stopImmediatePropagation()

O que é esse método estranho e pouco usado? Ele é semelhante a stopPropagation, mas, em vez de impedir que um evento seja transmitido para descendentes (captura) ou ancestrais (flutuação), esse método é aplicado apenas quando você tem mais de um gerenciador de eventos conectado a um único elemento. Como addEventListener() oferece suporte a um estilo de evento multicast, é possível conectar um gerenciador de eventos a um único elemento mais de uma vez. Quando isso acontece, na maioria dos navegadores, os manipuladores de eventos são executados na ordem em que foram conectados. Chamar stopImmediatePropagation() impede que os gerenciadores subsequentes sejam disparados. Veja o exemplo a seguir:

<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,
);

O exemplo acima resultará na seguinte saída do console:

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

O terceiro manipulador de eventos nunca é executado devido ao fato de que o segundo manipulador de eventos chama e.stopImmediatePropagation(). Se e.stopPropagation() fosse chamado, o terceiro gerenciador ainda seria executado.

event.preventDefault()

Se stopPropagation() impedir que um evento ocorra "para baixo" (capturando) ou "para cima" (balão), o que preventDefault() fará? Parece que ele faz algo semelhante. Será?

Na verdade, não. Embora os dois sejam frequentemente confundidos, eles não têm muito a ver um com o outro. Quando você vir preventDefault(), adicione a palavra "ação" na sua cabeça. Pense em "impedir a ação padrão".

E qual é a ação padrão que você pode pedir? Infelizmente, a resposta não é tão clara, porque depende muito da combinação de elemento + evento em questão. E, para tornar as coisas ainda mais confusas, às vezes não há nenhuma ação padrão.

Vamos começar com um exemplo muito simples. O que você espera que aconteça ao clicar em um link em uma página da Web? Obviamente, você espera que o navegador acesse o URL especificado por esse link. Nesse caso, o elemento é uma tag âncora e o evento é de clique. Essa combinação (<a> + click) tem uma "ação padrão" de navegar até o href do link. E se você quisesse impedir o navegador de realizar essa ação padrão? Ou seja, suponha que você queira impedir que o navegador navegue até o URL especificado pelo atributo href do elemento <a>. Isso é o que preventDefault() vai fazer por você. Por exemplo,

<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,
);

Você pode interagir com isso na demonstração ao vivo abaixo. Clique no link The Avett Brothers e observe a saída do console e o fato de que você não é redirecionado para o site da banda.

Normalmente, clicar no link "The Avett Brothers" resultaria na navegação para www.theavettbrothers.com. No entanto, neste caso, conectamos um gerenciador de eventos de clique ao elemento <a> e especificamos que a ação padrão precisa ser impedida. Assim, quando um usuário clicar nesse link, ele não será navegado para lugar algum. Em vez disso, o console apenas registrará "Talvez seja melhor tocar algumas das músicas dele aqui?".

Que outras combinações de elementos/eventos permitem que você impeça a ação padrão? Não é possível listar todos, e, às vezes, é preciso apenas testar para ver. Mas, brevemente, aqui estão alguns:

  • Elemento <form> + evento "submit": preventDefault() para essa combinação impede que um formulário seja enviado. Isso é útil se você quiser realizar a validação e, caso algo falhe, você pode chamar condicionalmente preventDefault para impedir o envio do formulário.

  • Elemento <a> + evento "click": preventDefault() para essa combinação impede que o navegador navegue para o URL especificado no atributo href do elemento <a>.

  • document + evento "mousewheel": preventDefault() para essa combinação impede a rolagem da página com a roda do mouse (a rolagem com o teclado ainda funciona).
    ↜ É necessário chamar addEventListener() com { passive: false }.

  • document + evento "keydown": preventDefault() para essa combinação é letal. Isso torna a página praticamente inútil, impedindo a rolagem, a tabulação e o destaque do teclado.

  • document + evento "mousedown": preventDefault() para essa combinação impede o realce do texto com o mouse e qualquer outra ação "padrão" que seria invocada com um mouse pressionado.

  • Elemento <input> + evento "keypress": preventDefault() para essa combinação vai impedir que os caracteres digitados pelo usuário cheguem ao elemento de entrada. Não faça isso, porque raramente há um motivo válido para isso.

  • document + evento "contextmenu": preventDefault() para essa combinação impede que o menu de contexto nativo do navegador apareça quando um usuário clica com o botão direito do mouse ou pressiona por muito tempo (ou qualquer outra forma em que um menu de contexto possa aparecer).

Esta não é uma lista completa, mas espero que ela dê uma boa ideia de como preventDefault() pode ser usado.

Uma pegadinha?

O que acontece se você stopPropagation() e preventDefault() na fase de captura, começando pelo documento? Hilaridade acontece! O snippet de código abaixo renderizará qualquer página da Web, tornando-a completamente inútil:

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 });

Não sei por que você faria isso (exceto para pregar uma peça em alguém), mas é útil pensar sobre o que está acontecendo aqui e perceber por que isso cria a situação que cria.

Todos os eventos se originam em window. Portanto, neste snippet, vamos interromper e desativar todos os eventos click, keydown, mousedown, contextmenu e mousewheel para nenhum elemento que possa estar detectando-os. Também chamamos stopImmediatePropagation para que todos os gerenciadores conectados ao documento depois desse também sejam impedidos.

Observe que stopPropagation() e stopImmediatePropagation() não são (pelo menos não em grande parte) o que tornam a página inútil. Eles simplesmente impedem que os eventos cheguem aonde iriam.

No entanto, também chamamos preventDefault(), que impede a action padrão. Assim, qualquer ação padrão (como rolagem da roda do mouse, rolagem do teclado ou destaque ou tabulação, clique em links, exibição do menu de contexto etc.) é impedida, deixando a página em um estado bastante inútil.

Demonstrações ao vivo

Para conferir todos os exemplos deste artigo em um só lugar, confira a demonstração embutida abaixo.

Agradecimentos

Imagem principal de Tom Wilson no Unsplash.