Arquitetura e Implementação do Sistema de Eventos no Vue.js

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.

Tags: Vue.js javascript Frontend Frameworks Event-Driven Programming

Publicado em 7-21 07:18