Desmitificando eventos no JavaScript

Summary #

Event Capture e Bubbling #

Capture e Bubbling é um conceito que depois que você aprende, começa a fazer bastante sentido alguns comportamentos inesperados, e se você tiver desenvolvendo sem biblioteca, são problemas que aparecem constantemente.

Quando você adiciona evento em um elemento, e o usuário clica em algo na tela, o comportamento default é de procurar o elemento mais internamente (capture), para só depois olhar para os de fora (bubbling) para propagar e disparar possíveis eventos que podem existir.

<body>
<div>
<form>
<button>send</button>
</form>
</div>
</body>

Nesse exemplo, se eu tenho um evento de click adicionado para div, form e button, se eu clicar no botão, por mais que ele esteja dentro desses outros, e os elements também tiverem evento de click, o evento do botão vai ser disparado primeiro, e só depois em seguida o form e a div, essa volta a gente chama de propagação ou de bubbling.

Stop propagation #

Você também pode parar essa propagação para que só o primeiro evento seja disparado:

const button = document.querySelector('button')
button.addEventListener('click', function onClick(e) {
e.stopPropagation()
});

Capture #

Ao invés dele pegar os eventos mais internos, você pode disparar eles na fase de captura:

const button = document.querySelector('button')
button.addEventListener('click', function onClick(e) {

}, { capture: true });

Assim quando ele tiver descendo na árvore, e encontrar algum evento, ele já dispara, ao invés de ir primeiro no mais interno, que é o comportamento default.

Once #

Outro argumento que as vezes pode ser útil, é o once, para que o evento seja disparado apenas uma vez.

const button = document.querySelector('button')
button.addEventListener('click', function onClick(e) {

}, { once: true });

Após clicar, o evento é removido.

Como escutar eventos ? #

Atualmente existem 3 maneiras de escutar eventos de um elemento:

Inline event handler #

Essa é uma má prática se você se preocupa com a legibilidade da marcação e a manutenção, principalmente quando estamos falando de uma aplicação de larga escala.

<button onclick="onClick()"> my button </button>

DOM Property #

Essa é parecida com a primeira, você basicamente tá acessando a propriedade do element <strong>onclick</strong> e adicionando a função, essa é melhor que a primeira porque está conseguindo separar da marcação, porém você não consegue atribuir múltiplos eventos de click por exemplo, no mesmo elemento.

const button = document.querySelector('.button')
button.onclick = () => console.log('clicked')

Event Listener #

Adicionar um listener é a maneira mais utilizada pois tem mais vantagens que as anteriores, uma delas é de remover um evento do elemento ou até mesmo adicionar vários eventos de click no mesmo elemento.

const button = document.querySelector('.button')
button.addEventListener('click', functionA)
button.addEventListener('click', functionB)

Se voce tentar fazer isso com o onclick, não vai funcionar, porque ele vai sobrescrever a função anterior.

Como criar evento próprio ? #

Ele pode ser criado utilizando o construtor Event ou CustomEvent:

const myCustomEvent = new Event('myevent', {
bubbles: true, //devo ou não propagar
cancelable: true, //quando usado, se eu posso ou nao cancelar este evento, como por exemplo usando o e.preventDefault().
})

const myCustomEvent2 = new CustomEvent('myevent', {
detail: {},
bubbles: true, //devo ou não propagar
cancelable: true, //quando usado, se eu posso ou nao cancelar este evento, como por exemplo usando o e.preventDefault().
})

Um detalhe que vale destacar, é que os eventos são case-insensitive, então esse nosso evento poderá ser chamado via: MyEvent, myEvent, myevent, etc.

A única diferenca é que o CustomEvent voce pode passar algum valor pela propriedade details quando for disparado o evento.

Como chamar meu evento ? #

Depois que criado seu evento, ele pode ser disparado a partir de qualquer elemento que extenda de um EventTarget, e eles estão inclusos em qualquer elemento HTML.

const myEvent = new CustomEvent("myevent", {
detail: {},
bubbles: true,
cancelable: true,
})

document.querySelector("#myElement").dispatchEvent(myEvent);

Depois que disparado, todo mundo que tiver escutando ele será executado. Para escutá-lo é bem simples:

document
.querySelector("#myElement")
.addEventListener("myevent", (event) => {
console.log('hello!!!');
});

Event Patterns #

Existem vários patterns conhecidos e muito usados no JavaScript, irei apresentá-los 2:

Delegação de eventos #

Todo framework JavaScript já vem com alguma maneira de lidar com os eventos, e por debaixo dos panos eles acabam resolvendo alguns problemas que muitas pessoas não sabem que existem.

1 - Performance #

Você já fez algum querySelectorAll pra uma classe que era de vários itens de uma lista e fez um .forEach pra adicionar alguns eventListener ?

const items = Array.from(document.querySelectorAll('li.child-item'))

items.forEach(item => {
item.addEventListener('click', () => {});
})

E se for mais de 100 itens ? Vai ser muito evento pendurado para o browser ter que gerenciar, e quanto mais eventos, mais memória precisa ser utilizada pelos browsers e sua página acaba ficando lenta.

2 - Adicionar eventos em nós dinamicos #

Se você adiciona os eventos assim que a página é montada, você vai ficar escutando os eventos, mas se for elementos no DOM que podem ser removidos ou adicionados, você vai ter que executar novamente essa função de adicionar eventos, pois os eventos só vão está nos elementos que existiam quando a página foi carregada.

Solução #

Pra resolver esses tipos de problemas, é usada a técnica de delegar eventos para o pai.

const items = Array.from(document.querySelector('ul.parent-list'))

items.addEventListener('click', (e) => {
if(e.target.nodeName === 'li') {

}
});

Assim você consegue ter apenas um evento no pai, que verifica qual filho foi clicado, e mesmo se for filhos dinâmicos não tem problema, pois como tá no pai, o evento sempre vai existir.

Publisher/Subscriber "Pub/Sub" #

É um padrão que nos permite criar módulos que podem se comunicar sem depender diretamente uns dos outros. É ótimo para desacoplamento.

Pra simplificar, vou utilizar uma biblioteca que já implementa este padrão, chamada de pubsub-js.

Podemos dizer que este padrao se baseia apenas na publicação e escuta de eventos, e temos duas funcões para isso: publish e subscribe. Exemplo:

import PubSub from 'pubsub-js'

const myButton = document.querySelector('.btn#edit')

myButton.addEventListener('click', function onClick(e) {
console.log('button clicked')
PubSub.publish('ACTION', { actionName: 'edit' })
})


//outro módulo desacoplado pode ficar só escutando:


(function Actions() {
const names = {
edit: () => {
console.log('edit my content')
},
save: () => {
console.log('save my content')
}
}

PubSub.subscribe('ACTION', ({ actionName }) => {
names[actionName]()
})
}())

Uma outra biblioteca muita famosa que se baseia nesse padrao, é o redux, onde é disparado uma action (publisher), e é executado um reducer (subscriber).

Cuidado #

Apesar de PubSub te dá esse poder de desacoplar sua aplicação, ele também abre possibilidade de ser difícil de entender o que está acontecendo quando sua aplicação tem um tamanho expressivo, então é sempre importante ter o cuidado de como está organizando o compartilhamento de dados e usar só quando realmente é necessário. Em SPA, é super comum um componente que está inscrito em um evento, precisar ser removido da tela, então precisa lembrar de remover a inscrição do evento, e assim não acabar tendo um gargalo de eventos pendurados desnecessário.

Conclusão #

No geral, eventos é o core do JavaScript na Web, tudo que o usuário interage, pode ser um evento, mas eu gosto de pensar que posso utilizar todos esses patterns e modelos de eventos para criar módulos e componentes com responsabilidade única e acoplamento baixo, e isso nos ajuda ter códigos reutilizáveis entre diferente módulos.

🙏🙏🙏

Já que você chegou até aqui, seria muito show compartilhar este artigo em sua rede social favorita 💖! Para feedback, comente ou interaja com emoji 👻

Published