跨瀏覽器的事件處理程序實現總結

本文章須要一些前置知識javascript

  1. 事件基礎知識java

  2. event對象詳解jquery

圍繞着如何更好地實現一個跨瀏覽器的事件處理小型庫展開討論。git

1. 初步實現

在《JavaScript高級程序設計》中提供了一個EventUtil的對象,裏面實現了一個跨瀏覽器的事件綁定的APIgithub

var EventUtil = {
    addHandler : function (el, type, handler) {
        if(el.addEventListener) {
            el.addEventListener(type, handler, false);
        } else if (el.attachEvent)(
            el.attachEvent("on" + type, handler);
        ) else {
            el["on" + type] = handler;
        }
    },
    removeHandler : function (el, type, handler) {
        if(el.removeEventListener) {
            el.removeEventListener(type, handler);
        } else if (el.detachEvent) {
            el.detachEvent("on" + type, handler);
        } else {
            el["on" + type] = null;
        }
    }
}

這是實現其實較爲的簡單直觀,可是對於IE瀏覽器的處理其實有很差的地方,例如咱們都知道attachEvent()中的事件處理程序會在全局做用域下執行,那麼函數中的this就會指向window對象,這是一個問題,固然咱們也能夠對handler進行處理,綁定handler的函數做用域。此外,EventUtil並無對event對象進行處理,所以傳入handler的event也須要作兼容性處理,在封裝方面作的就很差,編寫handler時須要注意的地方就比較多。web

var handler = function (event) {
    // 對event對象作兼容性處理,例如獲取target等
};

// 綁定函數做用域
handler = handler.bind(el);

2. 更好的實現

下面是Dean Edward的實現,這也是jquery所借鑑的,拋棄掉attachEvent方法,直接使用跨瀏覽器的實現方式,即el.onXXX = handler,這種方式的肯定就是沒法綁定多個,會進行覆蓋,可是能夠利用一些技巧來彌補。瀏覽器

// written by Dean Edwards, 2005
// with input from Tino Zijdel, Matthias Miller, Diego Perini
// http://dean.edwards.name/weblog/2005/10/add-event/

function addEvent(element, type, handler) {
    if (element.addEventListener) {
        element.addEventListener(type, handler, false);
    } else {
        // assign each event handler a unique ID
        if (!handler.$$guid) handler.$$guid = addEvent.guid++;
        // create a hash table of event types for the element
        if (!element.events) element.events = {};
        // create a hash table of event handlers for each element/event pair
        var handlers = element.events[type];
        if (!handlers) {
            handlers = element.events[type] = {};
            // store the existing event handler (if there is one)
            if (element["on" + type]) {
                handlers[0] = element["on" + type];
            }
        }
        // store the event handler in the hash table
        handlers[handler.$$guid] = handler;
        // assign a global event handler to do all the work
        element["on" + type] = handleEvent;
    }
};
// a counter used to create unique IDs
addEvent.guid = 1;

function removeEvent(element, type, handler) {
    if (element.removeEventListener) {
        element.removeEventListener(type, handler, false);
    } else {
        // delete the event handler from the hash table
        if (element.events && element.events[type]) {
            delete element.events[type][handler.$$guid];
        }
    }
};

function handleEvent(event) {
    var returnValue = true;
    // grab the event object (IE uses a global event object)
    event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
    // get a reference to the hash table of event handlers
    var handlers = this.events[event.type];
    // execute each event handler
    for (var i in handlers) {
        this.$$handleEvent = handlers[i];
        if (this.$$handleEvent(event) === false) {
            returnValue = false;
        }
    }
    return returnValue;
};

function fixEvent(event) {
    // add W3C standard event methods
    event.preventDefault = fixEvent.preventDefault;
    event.stopPropagation = fixEvent.stopPropagation;
    return event;
};
fixEvent.preventDefault = function() {
    this.returnValue = false;
};
fixEvent.stopPropagation = function() {
    this.cancelBubble = true;
};

這段代碼實際上是對IE瀏覽器事件綁定的一個修補,特別是舊版本的(IE8及更早的版本)。jquery借鑑了這樣的一個思路,寫出了兼容各個瀏覽器的event模塊。dom

3. jquery的實現思路

在《JavaScript忍者祕籍》中,給出了一個更加高級的實現,他使用一箇中間事件處理程序,並將全部的處理程序都保存在一個單獨的對象上,最大化地控制處理的過程,這樣作有幾個好處:函數

  • 規範處理程序的上下文,這個指的是做用域的問題,正常來講,元素的事件處理程序的上下文應該就是元素自己,即this === el爲true。性能

  • 修復Event對象的屬性,經過兼容性的處理,來達到與標準無異。

  • 處理垃圾回收

  • 過濾觸發或刪除一些處理程序

  • 解綁特定類型的全部事件

  • 克隆事件處理程序

依照這樣的一個思路,咱們來一步步實現這樣一個模塊。

3.1 修復Event對象的屬性

修復主要針對一些重要的屬性進行修復,結合上一節的內容,有如下代碼:

function fixEvent(event) {
    function returnTrue () {return true;}
    function returnFalse () {return false;}

    if(!event || !event.stopPropagation) { // 判斷是否須要修復
        var old = event || window.event; // IE的event從window對象中獲取

        event = {}; // 複製原有的event對象的屬性

        for(var prop in old) {
            event[prop] = old[prop];
        }

        // 處理target
        if(!event.target) {
            event.target = event.srcElement || document;
        }

        // 處理relatedTarget
        event.relatedTarget = event.fromElement === event.target ? 
                                        event.toElement : 
                                        event.fromElement;
        
        // 處理preventDefault
        event.preventDefault = function () {
            event.returnValue = false;
            // 標識,event對象是否調用了preventDefault函數
            event.isDefaultPrevented = returnTrue; 
        }
        /*
            能夠調用event.isDefaultPrevented()來查看是否調用event.preventDefault
        */
        event.isDefaultPrevented = returnFalse; 

        event.stopPropagation = function () {
            event.cancelBubble = true;
            event.isPropagationStopped = returnTrue;
        }

        event.isPropagationStopped = returnFalse;

        // 阻止事件冒泡,而且阻止執行其餘的事件處理程序
        // 藉助標識位,能夠在後面進行handlers隊列處理的時候使用
        event.stopImmediatePropagation = function () {
            event.isImmediatePropagationStopped = returnTrue;
            event.stopPropagation();
        }

        event.isImmediatePropagationStopped = returnFalse;

        // 鼠標座標,返回文檔座標
        if(event.clientX != null){
            var doc = document.documentElement, body = document.body;

            event.pageX = event.clientX +
                (doc && doc.scrollLeft || body && body.scrollLeft || 0) - 
                (doc && doc.clientLeft || body && body.clientLeft || 0);
            
            event.pageY = event.clientY +
                (doc && doc.scrollTop || body && body.scrollTop || 0) -
                (doc && doc.clientTop || body && body.clientTop || 0);
        }
    
        event.which = event.charCode || event.keyCode;

        // 鼠標點擊模式 left -> 0 middle -> 1 right -> 2
        if(event.button != null){
            event.button = (event.button & 1 ? 0 : 
                    (event.button & 4 ? 1 : 
                        (event.button & 2 ? 2 : 0)));
        }
    }
    return event;
}

3.2 中央對象保存dom元素信息

這個的目的是爲了給元素創建一個映射,標識元素和存儲相關聯的信息(事件類型和對應的事件處理程序),在jquery裏面使用的是selector,在《JavaScript忍者祕籍》中,使用的是guid。

var cache = {},
    guidCounter = 1,
    expando = "data" + (new Date).getTime();

function getData(el) {
    var guid = el[expando];
    if(!guid){
        guid = el[expando] = guidCounter++;
        cache[guid] = {};
    }
    return cache[guid];
}

function removeData(el) {
    var guid = el[expando];
    if(!guid) return;
    delete cache[guid];
    try {
        delete el[expando];
    } catch(e){
        if(el.removeAttribute){
            el.removeAttribute(expando);
        }
    }
}

3.3 綁定事件處理程序

var nextGuid = 1;

function addEvent(el, type, fn) {
    
    var data = getData(el);

    if(!data.handlers)data.handlers = {};

    if(!data.handlers[type])data.handlers[type] = [];

    // 給事件處理程序賦予guid,便於後面刪除
    if(!fn.guid)fn.guid = nextGuid++;

    data.handlers[type].push(fn);

    // 爲該元素的事件綁定統一的回調處理程序
    if(!data.dispatcher) {
        // 是否啓用data.dispatcher
        data.disabled = false;
        data.dispatcher = function (event) {
            if(data.disabled)return;
            event = fixEvent(event);
            var handlers = data.handlers[event.type];
            if(handlers) {
                for(var i = 0, len = handlers.length; i < len; i++){
                    handlers[i].call(el, event);
                }
            }
        };
    }

    // 將統一的回調處理程序註冊到,僅在第一次註冊的時候須要
    if(data.handlers.length === 1){
        if(el.addEventListener){
            el.addEventListener(type, data.dispatcher, false);
        } else (el.attachEvent) {
            el.attachEvent("on" + type, data.dispatcher);
        }
    }
}

3.4 清理資源

綁定了事件,就還須要一個解綁事件,由於咱們使用的是委託處理程序來控制處理流程,而不是直接綁定處理程序,因此也不能直接使用瀏覽器提供的解綁函數來處理。在這裏,咱們須要手動來清理一些資源,清理的順序從小到大。

function isEmpty(o){
    for(var prop in o){
        return false;
    }
    return true;
}

function tidyUp(el, type) {
    var data = getData(el);

    // 清理el的type事件的回調程序
    if(data.handlers[type].length === 0) {
        delete data.handlers[type];

        if(el.removeEventListener){
            el.removeEventListener(type, data.dispatcher, false);
        } else if(el.detachEvent){
            el.detachEvent("on" + type, data.dispatcher);
        }
    }

    // 判斷是否還有其餘類型的事件處理程序,若是沒有則進一步清除
    if(isEmpty(data.handlers)){
        delete data.handlers;
        delete data.dispatcher;
    }

    // 判斷是否還須要data對象
    if(isEmpty(data)) {
        removeData(el);
    }
}

3.5 解綁事件處理程序

爲了儘量保持靈活,提供瞭如下的功能

  • 將一個元素的全部綁定事件進行解綁

    removeEvent(el);
  • 將一個元素特定類型的全部事件進行解綁

    removeEvent(el, "click");
  • 將一個元素的特定處理程序進行解綁

    removeEvent(el, "click", handler);
function removeEvent(el, type, fn) {
    var data = getData(el);

    if(!data.handlers)return;

    var removeType = function(t) {
        data.handlers[t] = [];
        tidyUp(el, t);
    };

    // 刪除全部的處理程序
    if(!type){
        for(var t in data.handlers){
            removeType(t);
        }
        return;
    }

    var handlers = data.handlers[type];
    if(!handlers)return;

    // 刪除特定類型的全部事件處理程序
    if(!fn){
        removeType(type);
        return;
    }

    // 刪除特定的事件處理程序,這個時候根據guid來進行刪除
    // 這裏須要考慮的就是可能一個事件處理程序被綁定到一個事件類型屢次
    // 所以,這裏須要用到handlers.length,刪除的時候,須要n--
    if(fn.guid) {
        for(var n = 0; n < handlers.length; n++){ 
            if(handlers[n].guid === fn.guid){
                handlers.splice(n--, 1);
            }
        }
    }

    // 返回以前進行資源清理
    tidyUp(el, type);
}

到這裏,咱們就獲得一個既保證通用性又保證性能的事件監聽處理模塊,然而事件的知識並不單單這麼一點,本章節的內容將會繼續出如今接下來的幾個小節,一塊兒構建一個完整的event體系的代碼庫。

4. 參考

  1. 《JavaScript高級程序設計》

  2. 《JavaScript忍者祕籍》

  3. addEvent

5. 來源

我的博客

相關文章
相關標籤/搜索