Detalhamento do evento JavaScript

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

Event.stopPropagation() e Event.preventDefault()

O processamento de eventos em JavaScript costuma ser simples. Isso é especialmente verdadeiro quando se trata de uma estrutura HTML simples (relativamente plana). No entanto, as coisas ficam um pouco mais complicadas quando os eventos estão viajando (ou se propagando) por uma hierarquia de elementos. Normalmente, é quando os desenvolvedores usam stopPropagation() e/ou preventDefault() para resolver os problemas que estão enfrentando. Se você já pensou "Vou tentar preventDefault() e, se não funcionar, vou tentar stopPropagation() e, 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 deles e fornecer vários exemplos práticos para você analisar. Meu objetivo é acabar com sua confusão de uma vez por todas.

Antes de nos aprofundarmos muito, é importante abordar brevemente os dois tipos de processamento de eventos possíveis em JavaScript (em todos os navegadores modernos, ou seja, o Internet Explorer antes da versão 9 não oferecia suporte à captura de eventos).

Estilos de eventos (captura e propagação)

Todos os navegadores modernos são compatíveis com a captura de eventos, mas ela raramente é usada por desenvolvedores. Curiosamente, essa era a única forma de eventos que o Netscape oferecia suporte originalmente. O maior rival do Netscape, o Microsoft Internet Explorer, não oferecia suporte à captura de eventos, mas apenas a outro estilo de eventos chamado de bubbling de eventos. Quando o W3C foi formado, ele encontrou mérito nos dois estilos de eventos e declarou que os navegadores deveriam oferecer suporte a ambos, usando um terceiro parâmetro para o método addEventListener. Originalmente, esse parâmetro era apenas um booleano, mas todos os navegadores modernos são compatíveis com 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 e a propriedade capture são opcionais. Se um deles for omitido, o valor padrão de capture será false, ou seja, o bubbling de eventos será usado.

Captura de eventos

O que significa quando o gerenciador de eventos está "detectando na fase de captura"? Para entender isso, precisamos saber como os eventos surgem e como eles se propagam. O seguinte é verdadeiro para todos os eventos, mesmo que você, como desenvolvedor, não use, não se importe ou não pense nisso.

Todos os eventos começam na janela e passam primeiro 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 que você esteja apenas ouvindo 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 maneira:

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

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

No nosso exemplo, o evento de clique vai propagar (essa é uma palavra importante, já que ela se relaciona diretamente com o funcionamento do método stopPropagation(), que será explicado 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á algo detectando um evento de clique no window na fase de captura?" Se for o caso, os manipuladores de eventos adequados serão disparados. No nosso exemplo, nada é, então nenhum manipulador será acionado.

Em seguida, o evento vai propagar para o document, e o navegador vai perguntar: "Há algo ouvindo um evento de clique no document na fase de captura?" Se for o caso, os manipuladores de eventos adequados serão acionados.

Em seguida, o evento vai propagar para o elemento <html>, e o navegador vai perguntar: "Há algo aguardando um clique no elemento <html> na fase de captura?" Se for o caso, os manipuladores de eventos apropriados serão disparados.

Em seguida, o evento vai propagar para o elemento <body>, e o navegador vai perguntar: "Há algo aguardando um evento de clique no elemento <body> na fase de captura?" Se for o caso, os manipuladores de eventos adequados serão acionados.

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

Em seguida, o evento vai se propagar 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 aguardando um evento de clique no elemento #C na fase de captura?" Desta vez, a resposta é "sim". Esse breve período em que o evento está no destino é conhecido como "fase de destino". Nesse ponto, o processador 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.

Propagação de eventos

O navegador vai perguntar:

"Há algo detectando um evento de clique em #C na fase de bubbling?" Preste atenção aqui. É totalmente possível detectar cliques (ou qualquer tipo de evento) nas duas fases de captura e bubbling. E se você tivesse conectado 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 seriam acionados para o mesmo elemento. Mas também é importante observar que eles são disparados em fases diferentes (um na fase de captura e outro na fase de bubbling).

Em seguida, o evento vai propagar (mais comumente chamado de "bubble" porque parece que o evento está viajando "para cima" na árvore DOM) para o elemento pai, #B, e o navegador vai perguntar: "Há algo escutando eventos de clique em #B na fase de bubbling?" No nosso exemplo, nada é, então nenhum manipulador será acionado.

Em seguida, o evento vai passar para #A, e o navegador vai perguntar: "Há algo detectando eventos de clique em #A na fase de propagação?"

Em seguida, o evento vai passar para <body>: "Há algo detectando eventos de clique no elemento <body> na fase de propagação?"

Em seguida, o elemento <html>: "Há algo detectando eventos de clique no elemento <html> na fase de propagação?

Em seguida, o document: "Há algo detectando eventos de clique no document na fase de propagação?"

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

Ufa. Essa foi uma longa jornada, e nosso evento provavelmente está muito cansado agora, mas acredite ou não, essa é a jornada que todo evento passa! Na maioria das vezes, isso nunca é notado porque os desenvolvedores geralmente só se interessam por uma fase de evento ou outra (e geralmente é a fase de bubbling).

Vale a pena passar algum tempo brincando com a captura e o bubbling de eventos e registrando algumas observações no console à medida que os manipuladores são disparados. É muito útil ver 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 depende do elemento em que você clica. Se você clicar no elemento "mais profundo" na árvore DOM (o elemento #C), verá todos esses manipuladores de eventos serem acionados. Com um pouco de estilo CSS para deixar mais óbvio qual elemento é qual, aqui está o elemento de saída do console #C (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"

event.stopPropagation()

Agora que entendemos a origem e o percurso (ou seja, a propagação) dos eventos no DOM durante as fases de captura e bubbling, podemos nos concentrar em 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 é propagado desde o início). Eventos como focus, blur, load, scroll e alguns outros se enquadram nessa categoria. Você pode chamar stopPropagation(), mas nada de interessante vai acontecer, já que esses eventos não são propagados.

Mas o que o stopPropagation faz?

Ele faz, basicamente, o que diz. Quando você o chama, o evento deixa de se propagar para qualquer elemento que ele alcançaria. Isso é válido para ambas as direções (captura e propagação). Portanto, se você chamar stopPropagation() em qualquer lugar na fase de captura, o evento nunca chegará à fase de destino ou de propagação. Se você chamar na fase de propagação, ela já terá passado pela fase de captura, mas vai parar de "subir" a partir do ponto em que você a chamou.

Voltando ao mesmo exemplo de marcação, o que você acha que aconteceria se chamássemos stopPropagation() na fase de captura do elemento #B?

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"

Que tal interromper a propagação em #A na fase de bubbling? 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"

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á na meta. 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"

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

Em qualquer uma dessas demonstrações ao vivo, recomendo que você teste. Tente clicar apenas no elemento #A ou apenas no elemento body. Tente prever o que vai acontecer e observe se você está certo. Neste ponto, você já deve conseguir fazer previsões com bastante precisão.

event.stopImmediatePropagation()

O que é esse método estranho e pouco usado? É semelhante a stopPropagation, mas, em vez de impedir que um evento vá para descendentes (captura) ou ancestrais (propagação), esse método só se aplica quando você tem mais de um manipulador de eventos conectado a um único elemento. Como o addEventListener() oferece suporte a um estilo multicast de eventos, é totalmente possível conectar um processador 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 outros manipuladores 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 vai 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 porque o segundo chama e.stopImmediatePropagation(). Se tivéssemos chamado e.stopPropagation(), o terceiro manipulador ainda seria executado.

event.preventDefault()

Se stopPropagation() impedir que um evento viaje "para baixo" (captura) ou "para cima" (propagação), o que preventDefault() faz? Parece que ele faz algo parecido. É?

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, você pode perguntar? Infelizmente, a resposta não é tão clara porque depende muito da combinação de elemento e evento em questão. E para deixar tudo ainda mais confuso, às vezes não há nenhuma ação padrão.

Vamos começar com um exemplo bem simples para entender. O que você espera que aconteça quando clica em um link em uma página da Web? Obviamente, você espera que o navegador navegue até o URL especificado por esse link. Nesse caso, o elemento é uma tag de âncora, e o evento é um clique. Essa combinação (<a> + click) tem uma "ação padrão" de navegar até o href do link. E se você quisesse impedir que o navegador realizasse essa ação padrão? Por exemplo, suponha que você queira impedir que o navegador navegue até o URL especificado pelo atributo href do elemento <a>. É isso que o preventDefault() faz 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,
);

Normalmente, clicar no link "The Avett Brothers" resultaria na navegação até www.theavettbrothers.com. Neste caso, conectamos um manipulador de eventos de clique ao elemento <a> e especificamos que a ação padrão deve ser evitada. Assim, quando um usuário clicar nesse link, ele não será direcionado a lugar nenhum. Em vez disso, o console vai registrar "Talvez seja melhor tocar algumas músicas deles aqui mesmo?"

Quais outras combinações de elementos/eventos permitem impedir a ação padrão? Não é possível listar todos, e às vezes você só precisa testar para ver. Mas, resumidamente, aqui estão alguns:

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

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

  • Evento document + "mousewheel": preventDefault() para essa combinação impede a rolagem da página com a roda do mouse. No entanto, a rolagem com o teclado ainda funciona.
    ↜ Isso exige chamar addEventListener() com { passive: false }.

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

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

  • Elemento <input> + evento "keypress": preventDefault() para essa combinação impede que os caracteres digitados pelo usuário cheguem ao elemento de entrada. No entanto, não faça isso. Raramente, ou nunca, há um motivo válido para isso.

  • Evento document + "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 e mantém pressionado (ou qualquer outra maneira em que um menu de contexto possa aparecer).

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

Uma pegadinha divertida?

O que acontece se você stopPropagation() e preventDefault() na fase de captura, começando pelo documento? A diversão começa! O snippet de código a seguir vai tornar qualquer página da Web praticamente 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 (talvez para pregar uma peça em alguém), mas é útil pensar no que está acontecendo aqui e perceber por que isso cria a situação que cria.

Todos os eventos têm origem em window. Portanto, neste snippet, estamos interrompendo todos os eventos click, keydown, mousedown, contextmenu e mousewheel para que eles não cheguem a elementos que possam estar aguardando por eles. Também chamamos stopImmediatePropagation para que qualquer manipulador conectado ao documento depois deste também seja impedido.

stopPropagation() e stopImmediatePropagation() não são (pelo menos não na maioria dos casos) o que torna a página inútil. Elas simplesmente impedem que os eventos cheguem onde chegariam de outra forma.

Mas também chamamos preventDefault(), que, como você deve lembrar, impede a ação padrão. Assim, todas as ações padrão (como rolagem da roda do mouse, rolagem ou destaque do teclado, tabulação, clique em links, exibição do menu de contexto etc.) são impedidas, deixando a página em um estado bastante inútil.

Agradecimentos

Imagem principal de Tom Wilson no Unsplash (links em inglês).