Event delegation no javascript

Em meu primeiro post, comentei um pouco sobre os problemas de se referenciar nós criados dinamicamente no IE. Hoje vou apresentar uma segunda solução bastante interessante e econômica, quando pensamos em memory leak no IE, o event delegation ou delegação de eventos (tradução literal).

Para fazer uma breve introdução, este não é um tópico recente, a apresentação deste conceito já foi realizado por Peter Paul-Kock, Rob Cherny, Robert Nyman, Nicholas C. Zakas, Dan Webb, entre outros. Não vou me extender sobre a explicação das fases dos evento (captura e bubble), recomendo a vocês ler os posts sobre eventos publicados por Peter Paul-Kock.

A grande idéia por trás da delegação de eventos, é que não associamos mais os eventos aos elementos que os disparam e sim associamos o evento que desejamos avaliar a um ancestral e realizamos a análise deste posteriormente. Com isso ganhamos em memória alocada, ao invés de declararmos o evento para cada elemento que o dispara, o declaramos apenas uma vez para o ancestral, além de ganharmos na centralização dos eventos. Abaixo mostro o código relativo a uma comparação entre a forma tradicional de associarmos eventos e a delegação de eventos.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Event delegation</title>
        <style type="text/css">
            #container {
                background: #eaeaea;
                border: 1px solid #c0c0c0;
                margin: 10px auto;
                width: 960px;
                padding: 0;
            }
            #displayMessage, #botaoAdicao {
                width: 920px;
                margin: 10px auto;
            }
            #displayMessage {
                background: #fff;
                border: 1px solid #c0c0c0;
                color: #c0c0c0;
                text-align: center;
            }
            #botaoAdicao a {
                border: 1px solid #c0c0c0;
                background: #fff;
                font-weight: bold;
                padding: 0.125em;
                text-decoration: none;
            }
            #botaoAdicao a:link, #botaoAdicao a:visited {
                color: #039090;
            }
            #botaoAdicao a:hover {
                background: #f3f3f3;
                color: #36c3c3;
            }
            #botaoAdicao a:active {
                color: #ff0000;
            }
            .clearFloat {
                clear: both;
                line-height: normal;
                margin: 0;
                padding: 0;
            }
            #traditionalEvent, #delegationEvent {
                background: #fdfdfd;
                border-width: 0 1px 1px;
                border-style: solid;
                border-color: #c0c0c0;
                margin: 0;
                padding: 0;
                width: 45%;
            }
            #traditionalEvent {
                float: left;
                margin-left: 2.5%;
            }
            #delegationEvent {
                float: right;
                margin-right: 2.5%;
            }
            #traditionalEvent p, #delegationEvent p {
                font-weight: bold;
                background: #eaeaea;
                margin: 0 -1px;
                padding: 0 15em 0 0.5em;
                text-align: center;
                width: auto;
            }
            #traditionalEvent p span, #delegationEvent p span {
                background: #fdfdfd;
                border-width: 1px 1px 0;
                border-style: solid;
                border-color: #c0c0c0;
                display: block;
                margin: 0 0;
                padding: 0.25em 0 0.35em;
            }
            #traditionalEvent ul, #delegationEvent ul {
                margin: 0;
                padding: 0;
                list-style: none;
            }
            #traditionalEvent ul li, #delegationEvent ul li {
                margin: 0;
                padding: 0.25em 0.5em;
                border-top: 1px solid #c0c0c0;
            }
        </style>
    </head>
    <body>
        <div id="container">
            <div id="displayMessage"> </div>
            <div class="clearFloat"> </div>
            <div id="traditionalEvent">
                <p><span>Tradicional</span></p>
                <ul>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                </ul>
            </div>
            <div id="delegationEvent">
                <p><span>Delegation</span></p>
                <ul>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                    <li><input type="checkbox" /> <input type="text" disabled="disabled" value="" /></li>
                </ul>
            </div>
            <div class="clearFloat"> </div>
            <div id="botaoAdicao">
                <a href="#" title="teste de evento">Adicionar item</a>
            </div>
        </div>
        <script type="text/javascript">
<!--//--><![CDATA[//><!--
/**
 * Obtem o elemento que disparou o evento de interesse
 * @param {Object} e representa o objeto que lancou o evento
 */
function _getTarget(e){
    e = e || window.event;
    return e.target || e.srcElement;
}
/**
 * Funcao para faciliar a obtencao de elementos pelo atributo ID
 * @param {String} idElemento identidade do elemento
 */
function $(idElemento){
    return document.getElementById(idElemento);
}
/**
 * Funcao para padronizar o nome do elemento
 * @param {Object} elemento referencia ao objeto que se deseja obter o nome
 */
function _formataNomeNo(elemento){
    return elemento.nodeName.toUpperCase();
}
/**
 * Funcao auxiliar para criar um novo item de lista
 */
function _createLi(){
    var novoElemento = document.createElement('li');
    return novoElemento;
}
/**
 * Funcao auxiliar para criar um novo checkbox
 */
function _createCheckbox(){
    var novoElemento = document.createElement('input');
    novoElemento.type = 'checkbox';
    return novoElemento;
}
/**
 * Funcao auxiliar para criar um novo input text
 */
function _createInputText(){
    var novoElemento = document.createElement('input');
    novoElemento.type = 'text';
    novoElemento.disabled = true;
    novoElemento.value = '';
    return novoElemento;
}
/**
 * Funcao auxiliar para criar item complexo
 */
function _createLiForm(){
    var novoElemento = _createLi();
    novoElemento.appendChild(_createCheckbox());
    novoElemento.appendChild(document.createTextNode(" "));
    novoElemento.appendChild(_createInputText());
    return novoElemento;
}
/**
 * Funcao auxiliar para definir status do checkbox e passa-lo para o text
 * @param {Object} elementoPai elemento que contem os inputs
 */
function _eventInput(elementoPai){
    var inputFilho = elementoPai.getElementsByTagName('input');
    var inputCheckbox, inputText, inputTemp;
    for(var i = 0, j = inputFilho.length; i < j; i++){
        inputTemp = inputFilho[i];
        if(inputTemp.type == 'checkbox'){
            inputCheckbox = inputTemp;
        } else if(inputTemp.type == 'text'){
            inputText = inputTemp;
        }
    }
    if(inputCheckbox.checked){
        inputText.value = '00:00';
    } else {
        inputText.value = ' ';
    }
    inputText.disabled = !inputCheckbox.checked;
}
var divMensagem = $('displayMessage');
var divBotaoAdicao = $('botaoAdicao');
var divTradicional = $('traditionalEvent');
var divDelegation = $('delegationEvent');

divBotaoAdicao.onclick = function(e){
    //obtem referencia ao elemento que disparou o evento
    var elemento = _getTarget(e);
    if(_formataNomeNo(elemento) == 'A'){
        var ulTradicional = $('traditionalEvent').getElementsByTagName('ul')[0];
        var ulDelegation = $('delegationEvent').getElementsByTagName('ul')[0];
        ulTradicional.appendChild(_createLiForm());
        ulDelegation.appendChild(_createLiForm());
    }
    return false;
}

var liTradicional = divTradicional.getElementsByTagName('li');
for(var i = 0, j = liTradicional.length; i < j; i++){
    var liTemp = liTradicional[i];
    liTemp.onclick = function(){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento tradicional";
    };
}

var checkboxTradicional = divTradicional.getElementsByTagName('input');
for(var i = 0, j = checkboxTradicional.length; i < j; i++){
    var inputTemp = checkboxTradicional[i];
    if(inputTemp.type == 'checkbox'){
        inputTemp.onclick = function(){
            _eventInput(this.parentNode);
        };
    }
}

divDelegation.onclick = function(e){
    var elemento = _getTarget(e);
    if(_formataNomeNo(elemento) == 'LI'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
    } else if(_formataNomeNo(elemento) == 'INPUT' && elemento.type == 'checkbox'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
        var elementoPai = elemento.parentNode;
        _eventInput(elementoPai);
    }
};
//--><!]]>
        </script>
    </body>
</html>

Lógico que poderia ter separado as camadas de apresentação, lógica e de conteúdo, mas por simplicidade optei por deixar o código todo junto.

Agora vamos discutir os aspectos interessantes do códigos apresentado anteriormente:

var divTradicional = $('traditionalEvent');
Obtenho uma referência ao nó pai dos elementos que irão disparar em função do evento associado.
var liTradicional = divTradicional.getElementsByTagName('li');
for(var i = 0, j = liTradicional.length; i < j; i++){
    var liTemp = liTradicional[i];
    liTemp.onclick = function(){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento tradicional";
    };
}
Obtenho uma referência à array de elementos li aos quais desejo associar o comportamento quando clicados. Para conseguir associar o comportamento, realizo um loop por toda a lista e associo a cada elemento o evento onclick.
var checkboxTradicional = divTradicional.getElementsByTagName('input');
for(var i = 0, j = checkboxTradicional.length; i < j; i++){
    var inputTemp = checkboxTradicional[i];
    if(inputTemp.type == 'checkbox'){
        inputTemp.onclick = function(){
            _eventInput(this.parentNode);
        };
    }
}
Assim como realizado para os elementos li, repito o mesmo para os elementos input[type=checkbox].

Vamos ver como tudo é processado por meio do event delegation:

var divDelegation = $('delegationEvent');
Obtenho uma referência ao container dos elementos que irão disparar a função
divDelegation.onclick = function(e){
    var elemento = _getTarget(e);
    if(_formataNomeNo(elemento) == 'LI'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
    } else if(_formataNomeNo(elemento) == 'INPUT' && elemento.type == 'checkbox'){
        divMensagem.innerHTML = "";
        divMensagem.innerHTML = "Clicou em um elemento com evento delegado";
        var elementoPai = elemento.parentNode;
        _eventInput(elementoPai);
    }
};
Diferente do que foi feito para os elementos que receberam os eventos tradicionais, na delegação de eventos associo ao nó ancestral o evento onclick. A partir dele avaliarei qual elemento esta disparando o evento (por meio das condições if, e caso eles sejam os elementos de interesse disparo as funções que deveriam ser executadas.

Podemos verificar que a quantidade de código é bastante inferior, além disso, a quantidade de eventos associados é muito menor.

Na associção de eventos na forma tradicional precisariamos de um loop para definirmos os eventos sendo avaliados, no caso da delegação, só precisamos associar aos elementos chaves. Isso torna a manutenção do código também mais simples, podemos perceber facilmente a qual elemento o evento está associado e podemos adicionar mais facilmente outros comportamentos

Posso dizer, em minha prática, que a delegação de eventos tem me poupado muito trabalho em meus códigos, além de evitar que eu tenha problemas com o IE, uma vez que tenho uma quantidade de eventos muito menor a mapear, o que torna bastante improvável o memory leak.

Até o próximo post

~ por joaodubas em 26/08/2009.

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

 
Seguir

Obtenha todo post novo entregue na sua caixa de entrada.

%d blogueiros gostam disto: