Detalhamento do evento JavaScript

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

Event.stopPropagation() e Event.preventDefault()

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 preventDefault() e, se não funcionar, vou tentar stopPropagation(). Se 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 uma variedade de exemplos de trabalho para você conferir. 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 apenas 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 se movem. 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 para os descendentes da seguinte maneira:

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 meio de 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 acionados. 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 vai ser propagado para o elemento <html>, e o navegador vai perguntar: "Há algo aguardando 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?" Se sim, os manipuladores de eventos adequados serão acionados.

Em seguida, o evento vai 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 é na meta é conhecido como "fase de destino". Nesse ponto, o gerenciador de eventos será acionado, o navegador vai console.log "#C was clicked" e pronto, certo? Errado. Ainda não terminamos. O processo continua, mas agora muda para a fase de bubbling.

Fluxo de eventos

O navegador vai perguntar:

"Há algo detectando um evento de clique em #C na fase de bubbling?" Preste atenção. É totalmente possível detectar cliques (ou qualquer tipo de evento) nas fases de captura e bubbling. Se você tiver conectado os manipuladores de eventos nas duas fases (por exemplo, chamando .addEventListener() duas vezes, uma com capture = true e outra com capture = false), então sim, os dois manipuladores de eventos serão acionados 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?" No nosso exemplo, nada é, então nenhum manipulador é acionado.

Em seguida, o evento vai ser transmitido para #A, e o navegador vai perguntar: "Algo 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: "Há algo 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 passa! 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 útil conferir o caminho que um evento faz. 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 os manipuladores de eventos serão acionados. Com um pouco de estilo CSS para deixar mais óbvio qual elemento é qual, aqui está o elemento #C da 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 interagir com isso na demonstração ao vivo abaixo. Clique no elemento #C e observe a saída do console.

event.stopPropagation()

Com o entendimento de onde os eventos se originam e como eles viajam (ou seja, se propagam) pelo DOM na fase de captura e na fase de bubbling, podemos agora 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á alguns em que chamar esse método não faz nada (porque o evento não se propaga 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 na fase de captura, o evento nunca chegará à fase de destino ou de bolha. Se você chamar na fase de bolha, ela já terá passado pela fase de captura, mas vai deixar de "subir" a partir do ponto em que você chamou.

Voltando ao mesmo exemplo de marcação, o que você acha que aconteceria se chamássemos 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 bolhas? 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? "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 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.

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 momento, você deve conseguir 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 (bolha), esse método só se aplica 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. A chamada de stopImmediatePropagation() impede que outros manipuladores sejam acionados. 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 gerenciador de eventos nunca é executado porque o segundo gerenciador de eventos chama e.stopImmediatePropagation(). Se e.stopPropagation() fosse chamado, o terceiro gerenciador ainda seria executado.

event.preventDefault()

Se stopPropagation() impedir que um evento viaje "para baixo" (captura) ou "para cima" (bolha), o que preventDefault() faz? Parece que ele faz algo semelhante. Será que sim?

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 deixar 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 quando clica 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 clica nesse link, ele não é direcionado a lugar nenhum. Em vez disso, o console simplesmente registra "Talvez devêssemos tocar algumas músicas dele aqui mesmo?"

Quais outras combinações de elementos/eventos permitem impedir a ação padrão? Não é possível listar todos eles, e às vezes você precisa testar para conferir. Mas, resumidamente, 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 uma 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).
    ↜ Para isso, é 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).

Essa lista não é exaustiva, mas pode dar uma ideia de como preventDefault() pode ser usado.

Uma pegadinha divertida?

O que acontece se você stopPropagation() e preventDefault() na fase de captura, começando no 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, estamos impedindo que todos os eventos click, keydown, mousedown, contextmenu e mousewheel cheguem a qualquer elemento que possa estar detectando esses eventos. Também chamamos stopImmediatePropagation para que todos os gerenciadores conectados ao documento após esse também sejam impedidos.

stopPropagation() e stopImmediatePropagation() não são (pelo menos não na maioria dos casos) o que tornam a página inútil. Eles simplesmente impedem que os eventos cheguem aonde iriam.

Mas também chamamos preventDefault(), que impede a ação 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.