O sistema de eventos do Vue.js é um componente fundamental para a comunicação entre componentes e a gestão de estados reativos. A lógica principal reside no diretório src/core/instance/events.js, mas ela depende fortemante de utilitários de snicronização localizados em src/core/vdom/helpers/update-listeners.js. Abaixo, exploramos como o framework gerencia o ciclo de vida dos eventos, desde a inicilaização até o disparo.
Sincronização de Listeners
A função updateListeners é o motor que reconcilia os ouvintes de eventos antigos com os novos. Ela é agnóstica quanto à plataforma, delegando a adição e remoção real de eventos para funções injetadas (add e remove).
/**
* Sincroniza a coleção de listeners de um componente.
* @param {Object} novosListeners - Objeto com os novos eventos
* @param {Object} antigosListeners - Objeto com os eventos anteriores
* @param {Function} adicionar - Handler para registrar o evento
* @param {Function} remover - Handler para excluir o evento
* @param {Function} criarHandlerUnico - Função para eventos '.once'
* @param {Component} instancia - A instância do componente Vue
*/
export function gerenciarListeners (
novosListeners,
antigosListeners,
adicionar,
remover,
criarHandlerUnico,
instancia
) {
let chave, def, atual, anterior, infoEvento
for (chave in novosListeners) {
def = atual = novosListeners[chave]
anterior = antigosListeners[chave]
// Normaliza o nome do evento para identificar modificadores (~, !, &)
infoEvento = extrairDadosEvento(chave)
if (isUndef(atual)) {
process.env.NODE_ENV !== 'production' && warn(
`Handler inválido para o evento "${infoEvento.name}"`,
instancia
)
} else if (isUndef(anterior)) {
// Caso seja um novo listener
if (isUndef(atual.fns)) {
atual = novosListeners[chave] = criarInvocador(atual, instancia)
}
// Suporte para o modificador .once
if (isTrue(infoEvento.once)) {
atual = novosListeners[chave] = criarHandlerUnico(infoEvento.name, atual, infoEvento.capture)
}
adicionar(infoEvento.name, atual, infoEvento.capture, infoEvento.passive, infoEvento.params)
} else if (atual !== anterior) {
// Se o handler mudou, apenas atualizamos a referência interna sem re-registrar o evento
anterior.fns = atual
novosListeners[chave] = anterior
}
}
// Remove listeners que não existem mais no novo estado
for (chave in antigosListeners) {
if (isUndef(novosListeners[chave])) {
infoEvento = extrairDadosEvento(chave)
remover(infoEvento.name, antigosListeners[chave], infoEvento.capture)
}
}
}
Inicialização de Instância
Cada componente Vue possui um objeto privado _events onde os callbacks são armazenados. Durante a criação do componente, os listeners passados pelo componente pai são processados.
let alvoAtual = null
function vincular (evento, fn) {
alvoAtual.$on(evento, fn)
}
function desvincular (evento, fn) {
alvoAtual.$off(evento, fn)
}
export function inicializarEventos (vm) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// Recupera listeners definidos no componente pai (ex: @custom-event="handle")
const ouvintesPai = vm.$options._parentListeners
if (ouvintesPai) {
atualizarOuvintesComponente(vm, ouvintesPai)
}
}
export function atualizarOuvintesComponente (vm, ouvintes, antigos) {
alvoAtual = vm
gerenciarListeners(ouvintes, antigos || {}, vincular, desvincular, criarHandlerUnico, vm)
alvoAtual = undefined
}
function criarHandlerUnico (nome, fn) {
const _instancia = alvoAtual
return function wrapper () {
const retorno = fn.apply(null, arguments)
if (retorno !== null) {
_instancia.$off(nome, wrapper)
}
}
}
O Mixin de Eventos: $on, $once, $off e $emit
A função eventsMixin estende o protótipo do Vue para fornecer a interface programática de eventos. Diferente de eventos nativos do DOM, esses eventos operam puramente em memória dentro da instância do componente.
export function injetarMixinEventos (Vue) {
const regexHook = /^hook:/
// Registra um ou mais ouvintes
Vue.prototype.$on = function (evento, fn) {
const vm = this
if (Array.isArray(evento)) {
evento.forEach(e => vm.$on(e, fn))
} else {
(vm._events[evento] || (vm._events[evento] = [])).push(fn)
// Otimização para eventos de lifecycle (hook:mounted, etc)
if (regexHook.test(evento)) {
vm._hasHookEvent = true
}
}
return vm
}
// Registra um ouvinte que se auto-destrói após a primeira execução
Vue.prototype.$once = function (evento, fn) {
const vm = this
function wrapper () {
vm.$off(evento, wrapper)
fn.apply(vm, arguments)
}
wrapper.fn = fn // Referência para permitir remoção manual via $off
vm.$on(evento, wrapper)
return vm
}
// Remove ouvintes de eventos
Vue.prototype.$off = function (evento, fn) {
const vm = this
// Sem argumentos: limpa todos os eventos da instância
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// Suporte para array de nomes de eventos
if (Array.isArray(evento)) {
evento.forEach(e => vm.$off(e, fn))
return vm
}
const callbacks = vm._events[evento]
if (!callbacks) return vm
// Se nenhum handler específico for passado, remove todos para este evento
if (!fn) {
vm._events[evento] = null
return vm
}
// Busca e remove o handler específico do array
let i = callbacks.length
while (i--) {
const cb = callbacks[i]
if (cb === fn || cb.fn === fn) {
callbacks.splice(i, 1)
break
}
}
return vm
}
// Dispara um evento local
Vue.prototype.$emit = function (nomeEvento) {
const vm = this
let callbacks = vm._events[nomeEvento]
if (callbacks) {
callbacks = callbacks.length > 1 ? Array.from(callbacks) : callbacks
const params = Array.from(arguments).slice(1)
for (let i = 0, len = callbacks.length; i < len; i++) {
// Executa o callback tratando possíveis erros de forma segura
executarCallback(callbacks[i], vm, params, vm, `handler para ${nomeEvento}`)
}
}
return vm
}
}
Funcionamento Interno
O sistema de eventos do Vue funciona como um padrão de Observer customizado. Quando um componente pai utiliza um componente filho e adiciona um ouvinte com v-on, esses listeners são anexados à instância do filho durante sua inicialização. Assim, quando o componente filho invoca this.$emit('evento'), ele está meramente percorrendo sua própria lista interna de funções (_events['evento']) e as executando. Não há borbulhamento (bubbling) de eventos para o DOM, o que torna o processo altamente performático.