JavaScript事件機制相容性解決方案

我的小樹林發表於2015-03-09

本文的解決方案可以用於Javascript native物件和宿主物件(dom元素),通過以下的方式來繫結和觸發事件:

或者

var input = document.getElementsByTagName('input')[0];
var form = document.getElementsByTagName('form')[0];
Evt.on(input, 'click', function(evt){
    console.log('input click1');
    console.log(evt.target === input);
    console.log(evt.modified);
    //evt.stopPropagation();
    console.log(evt.modified);
});
var handle2 = Evt.on(input, 'click', function(evt){
    console.log('input click2');
    console.log(evt.target === input);
    console.log(evt.modified);
});
Evt.on(form, 'click', function(evt){
    console.log('form click');
    console.log(evt.currentTarget === input);
    console.log(evt.target === input);
    console.log(evt.currentTarget === form);
    console.log(evt.modified);
});
Evt.emit(input, 'click');
Evt.emit(input, 'click', {bubbles: true});
handle2.remove();
Evt.emit(input, 'click');

After函式

為native物件新增事件的過程主要在after函式中完成,這個函式主要做了以下幾件事:

  1. 如果obj中已有響應函式,將其替換成dispatcher函式
  2. 使用鏈式結構,保證多次繫結事件函式的順序執行
  3. 返回一個handle物件,呼叫remove方法可以去除本次事件繫結

下圖為after函式呼叫前後onlog函式的引用

(呼叫前)

(呼叫後)

詳細解釋請看註釋,希望讀者能夠跟著執行一遍

var after = function(target, method, cb, originalArgs){
    var existing = target[method];
    var dispatcher = existing;
    if (!existing || existing.target !== target) {
        //如果target中沒有method方法,則為他新增一個方法method方法
        //如果target已經擁有method方法,但target[method]中target不符合要求則將method方法他替換
        dispatcher = target[method] = function(){
            //由於js是此法作用域:通過閱讀包括變數定義在內的數行原始碼就能知道變數的作用域。
            //區域性變數在宣告它的函式體內以及其所巢狀的函式內始終是有定義的
            //所以在這個函式中可以訪問到dispatcher變數
            var results = null;
            var args = arguments;
            if (dispatcher.around) {//如果原先擁有method方法,先呼叫原始method方法
                //此時this關鍵字指向target所以不用target
                results = dispatcher.around.advice.apply(this, args);
            }

            if (dispatcher.after) {//如果存在after鏈則依次訪問其中的advice方法
                var _after = dispatcher.after;
                while(_after && _after.advice) {
                    //如果需要原始引數則傳入arguments否則使用上次執行結果作為引數
                    args = _after.originalArgs ? arguments : results;
                    results = _after.advice.apply(this, args);
                    _after = _after.next;
                }
            }
        }

        if (existing) {
        //函式也是物件,也可以擁有屬性跟方法
        //這裡將原有的method方法放到dispatcher中
            dispatcher.around = {
                advice: function(){
                    return existing.apply(target, arguments);
                }
            }
        }
        dispatcher.target = target;
    }

    var signal = {
        originalArgs: originalArgs,//對於每個cb的引數是否使用最初的arguments
        advice: cb,
        remove: function() {
            if (!signal.advice) {
                return;
            }
            //remove的本質是將cb從函式鏈中移除,刪除所有指向他的連結
            var previous = signal.previous;
            var next = signal.next;
            if (!previous && !next) {
                dispatcher.after = signal.advice = null;
                dispatcher.target = null;
                delete dispatcher.after;
            } else if (!next){
                signal.advice = null;
                previous.next = null;
                signal.previous = null;
            } else if (!previous){
                signal.advice = null;
                dispatcher.after = next;
                next.previous = null;
                signal.next = null;
            } else {
                signal.advice = null;
                previous.next = next;
                next.previous = previous;
                signal.previous = null;
                signal.next = null;
            }
        }
    }

    var previous = dispatcher.after;
    if (previous) {//將signal加入到鏈式結構中,處理指標關係
        while(previous && previous.next && (previous = previous.next)){};
        previous.next = signal;
        signal.previous = previous;
    } else {//如果是第一次使用呼叫after方法,則dispatcher的after屬性指向signal
        dispatcher.after = signal;
    }

    cb = null;//防止記憶體洩露
    return signal;
}

解決相容性

IE瀏覽器從IE9開始已經支援DOM2事件處理程式,但是對於老版本的ie瀏覽器,任然使用attachEvent方式來為dom元素新增事件。值得慶幸的是微軟已宣佈2016年將不再對ie8進行維護,對於廣大前端開發者無疑是一個福音。然而在曙光來臨之前,仍然需要對那些不支援DOM2級事件處理程式的瀏覽器進行相容性處理,通常需要處理以下幾點:

  1. 多次繫結一個事件,事件處理函式的呼叫順序問題
  2. 事件處理函式中的this關鍵字指向問題
  3. 標準化event事件物件,支援常用的事件屬性

 

由於使用attachEvent方法新增事件處理函式無法保證事件處理函式的呼叫順序,所以我們棄用attachEvent,轉而用上文中的after生成的正序鏈式結構來解決這個問題。

//1、統一事件觸發順序
    function fixAttach(target, type, listener) {
    debugger;
        var listener = fixListener(listener);
        var method = 'on' + type;
        return after(target, method, listener, true);
    };

對於事件處理函式中的this關鍵字指向,通過閉包即可解決(出處),如:

本文也是通過這種方式解決此問題

//1、統一事件觸發順序
    function fixAttach(target, type, listener) {
    debugger;
        var listener = fixListener(listener);
        var method = 'on' + type;
        return after(target, method, listener, true);
    };

    function fixListener(listener) {
        return function(evt){
            //每次呼叫listenser之前都會呼叫fixEvent
            debugger;
            var e = _fixEvent(evt, this);//this作為currentTarget
            if (e && e.cancelBubble && (e.currentTarget !== e.target)){
                return;
            }
            var results =  listener.call(this, e);

            if (e && e.modified) {
                // 在整個函式鏈執行完成後將lastEvent迴歸到原始狀態,
                //利用非同步佇列,在主程式執行完後再執行事件佇列中的程式程式碼
                //常規的做法是在emit中判斷lastEvent並設為null
                //這充分體現了js非同步程式設計的優勢,把變數賦值跟清除程式碼放在一起,避免邏輯分散,缺點是不符合程式設計師正常思維方式
                if(!lastEvent){
                    setTimeout(function(){
                        lastEvent = null;
                    });
                }
                lastEvent = e;
            }
            return results;
        }
    }

對於事件物件的標準化,我們需要將ie提供給我們的現有屬性轉化為標準的事件屬性。

function _fixEvent(evt, sender){
        if (!evt) {
            evt = window.event;
        }
        if (!evt) { // emit沒有傳遞事件引數,或者通過input.onclick方式呼叫
            return evt;
        }
        if(lastEvent && lastEvent.type && evt.type == lastEvent.type){
        //使用一個全域性物件來保證在冒泡過程中訪問的是同一個event物件
        //chrome中整個事件處理過程event是唯一的
            evt = lastEvent;
        }
        var fixEvent = evt;
        // bubbles 和cancelable根據每次emit時手動傳入引數設定
        fixEvent.bubbles = typeof evt.bubbles !== 'undefined' ? evt.bubbles : false;
        fixEvent.cancelable = typeof evt.cancelable !== 'undefined' ? evt.cancelable : true;
        fixEvent.currentTarget = sender;
        if (!fixEvent.target){ // 多次繫結統一事件,只fix一次
            fixEvent.target = fixEvent.srcElement || sender;

            fixEvent.eventPhase = fixEvent.target === sender ? 2 : 3;
            if (!fixEvent.preventDefault) {
                fixEvent.preventDefault = _preventDefault;
                fixEvent.stopPropagation = _stopPropagation;
                fixEvent.stopImmediatePropagation = _stopImmediatePropagation;
            }
            //參考:http://www.nowamagic.net/javascript/js_EventMechanismInDetail.php
            if( fixEvent.pageX == null && fixEvent.clientX != null ) {
                var doc = document.documentElement, body = document.body;
                fixEvent.pageX = fixEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
                fixEvent.pageY = fixEvent.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
            }
            if (!fixEvent.relatedTarget && fixEvent.fromEvent) {
                fixEvent.relatedTarget = fixEvent.fromEvent === fixEvent.target ? fixEvent.toElement : fixEvent.fromElement;
            }
            // 參考: http://www.cnblogs.com/hsapphire/archive/2009/12/18/1627047.html
            if (!fixEvent.which && fixEvent.keyCode) {
                fixEvent.which = fixEvent.keyCode;
            }
        }

        return fixEvent;
    }

    function _preventDefault(){
        this.defaultPrevented = true;
        this.returnValue = false;

        this.modified = true;
    }

    function _stopPropagation(){
        this.cancelBubble = true;

        this.modified = true;
    }

    function _stopImmediatePropagation(){
        this.isStopImmediatePropagation = true;
        this.modified = true;
    }

在_preventDefault、_stopPropagation、_stopImmediatePropagation三個函式中我們,如果被呼叫則listener執行完後使用一個變數儲存event物件(見fixListener),以便後序事件處理程式根據event物件屬性進行下一步處理。stopImmediatePropagation函式,對於這個函式的模擬,我們同樣通過閉包來解決。

注意這裡不能直接寫成這種形式,上文中fixListener也是同樣道理。

需要注意一點,我們將event標準化目的還有一點,可以在emit方法中設定引數來控制事件過程,比如:

Evt.emit(input, ’click’);//不冒泡

Evt.emit(input, ’click’, {bubbles: true});//冒泡

根據我的測試使用fireEvent方式觸發事件,無法設定{bubbles:false}來阻止冒泡,所以這裡我們用Javascript來模擬冒泡過程。同時在這個過程中也要保證event物件的唯一性。

// 模擬冒泡事件
    var sythenticBubble = function(target, type, evt){
        var method = 'on' + type;
        var args = Array.prototype.slice.call(arguments, 2);
        // 保證使用emit觸發dom事件時,event的有效性
        if ('parentNode' in target) {
            var newEvent = args[0] = {};
            for (var p in evt) {
                newEvent[p] = evt[p];
            }

            newEvent.preventDefault = _preventDefault;
            newEvent.stopPropagation = _stopPropagation;
            newEvent.stopImmediatePropagation = _stopImmediatePropagation;
            newEvent.target = target;
            newEvent.type = type;
        }

        do{
            if (target && target[method]) {
                target[method].apply(target, args);
            }
        }while(target && (target = target.parentNode) && target[method] && newEvent && newEvent.bubbles);
    }

    var emit = function(target, type, evt){
        if (target.dispatchEvent && document.createEvent){
            var newEvent = document.createEvent('HTMLEvents');
            newEvent.initEvent(type, evt && !!evt.bubbles, evt && !!evt.cancelable);
            if (evt) {
                for (var p in evt){
                    if (!(p in newEvent)){
                        newEvent[p] = evt[p];
                    }
                }
            }

            target.dispatchEvent(newEvent);
        } /*else if (target.fireEvent) {
            target.fireEvent('on' + type);// 使用fireEvent在evt引數中設定bubbles:false無效,所以棄用
        } */else {
            return sythenticBubble.apply(on, arguments);
        }
    }

附上完整程式碼:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta http-equiv="window-target" content="_top">
<title>Writing to Same Doc</title>
<script language="JavaScript">
var after = function(target, method, cb, originalArgs){
    var existing = target[method];
    var dispatcher = existing;
    if (!existing || existing.target !== target) {
        //如果target中沒有method方法,則為他新增一個方法method方法
        //如果target已經擁有method方法,但target[method]中target不符合要求則將method方法他替換
        dispatcher = target[method] = function(){
            //由於js是此法作用域:通過閱讀包括變數定義在內的數行原始碼就能知道變數的作用域。
            //區域性變數在宣告它的函式體內以及其所巢狀的函式內始終是有定義的
            //所以在這個函式中可以訪問到dispatcher變數
            var results = null;
            var args = arguments;
            if (dispatcher.around) {//如果原先擁有method方法,先呼叫原始method方法
                //此時this關鍵字指向target所以不用target
                results = dispatcher.around.advice.apply(this, args);
            }

            if (dispatcher.after) {//如果存在after鏈則依次訪問其中的advice方法
                var _after = dispatcher.after;
                while(_after && _after.advice) {
                    //如果需要原始引數則傳入arguments否則使用上次執行結果作為引數
                    args = _after.originalArgs ? arguments : results;
                    results = _after.advice.apply(this, args);
                    _after = _after.next;
                }
            }
        }

        if (existing) {
        //函式也是物件,也可以擁有屬性跟方法
        //這裡將原有的method方法放到dispatcher中
            dispatcher.around = {
                advice: function(){
                    return existing.apply(target, arguments);
                }
            }
        }
        dispatcher.target = target;
    }

    var signal = {
        originalArgs: originalArgs,//對於每個cb的引數是否使用最初的arguments
        advice: cb,
        remove: function() {
            if (!signal.advice) {
                return;
            }
            //remove的本質是將cb從函式鏈中移除,刪除所有指向他的連結
            var previous = signal.previous;
            var next = signal.next;
            if (!previous && !next) {
                dispatcher.after = signal.advice = null;
                dispatcher.target = null;
                delete dispatcher.after;
            } else if (!next){
                signal.advice = null;
                previous.next = null;
                signal.previous = null;
            } else if (!previous){
                signal.advice = null;
                dispatcher.after = next;
                next.previous = null;
                signal.next = null;
            } else {
                signal.advice = null;
                previous.next = next;
                next.previous = previous;
                signal.previous = null;
                signal.next = null;
            }
        }
    }

    var previous = dispatcher.after;
    if (previous) {//將signal加入到鏈式結構中,處理指標關係
        while(previous && previous.next && (previous = previous.next)){};
        previous.next = signal;
        signal.previous = previous;
    } else {//如果是第一次使用呼叫after方法,則dispatcher的after屬性指向signal
        dispatcher.after = signal;
    }

    cb = null;//防止記憶體洩露
    return signal;
}

//1、統一事件觸發順序
//2、標準化事件物件
//3、模擬冒泡 emit時保持冒泡行為,注意input.onclick這種方式是不冒泡的
//4、保持冒泡過程中event的唯一性

window.Evt = (function(){
    var on = function(target, type, listener){
    debugger;
        if (!listener){
            return;
        }
        // 處理stopImmediatePropagation,通過包裝listener來支援stopImmediatePropagation
        if (!(window.Event && window.Event.prototype && window.Event.prototype.stopImmediatePropagation)) {
            listener = _addStopImmediate(listener);
        }

        if (target.addEventListener) {
            target.addEventListener(type, listener, false);

            return {
                remove: function(){
                    target.removeEventListener(type, listener);
                }
            }
        } else {
            return fixAttach(target, type, listener);
        }
    };
    var lastEvent; // 使用全域性變數來保證一個元素的多個listenser中事件物件的一致性,冒泡過程中事件物件的一致性;在chrome這些過程中使用的是同一個event
    //1、統一事件觸發順序
    function fixAttach(target, type, listener) {
    debugger;
        var listener = fixListener(listener);
        var method = 'on' + type;
        return after(target, method, listener, true);
    };

    function fixListener(listener) {
        return function(evt){
            //每次呼叫listenser之前都會呼叫fixEvent
            debugger;
            var e = _fixEvent(evt, this);//this作為currentTarget
            if (e && e.cancelBubble && (e.currentTarget !== e.target)){
                return;
            }
            var results =  listener.call(this, e);

            if (e && e.modified) {
                // 在整個函式鏈執行完成後將lastEvent迴歸到原始狀態,
                //利用非同步佇列,在主程式執行完後再執行事件佇列中的程式程式碼
                //常規的做法是在emit中判斷lastEvent並設為null
                //這充分體現了js非同步程式設計的優勢,把變數賦值跟清除程式碼放在一起,避免邏輯分散,缺點是不符合程式設計師正常思維方式
                if(!lastEvent){
                    setTimeout(function(){
                        lastEvent = null;
                    });
                }
                lastEvent = e;
            }
            return results;
        }
    }

    function _fixEvent(evt, sender){
        if (!evt) {
            evt = window.event;
        }
        if (!evt) { // emit沒有傳遞事件引數,或者通過input.onclick方式呼叫
            return evt;
        }
        if(lastEvent && lastEvent.type && evt.type == lastEvent.type){
        //使用一個全域性物件來保證在冒泡過程中訪問的是同一個event物件
        //chrome中整個事件處理過程event是唯一的
            evt = lastEvent;
        }
        var fixEvent = evt;
        // bubbles 和cancelable根據每次emit時手動傳入引數設定
        fixEvent.bubbles = typeof evt.bubbles !== 'undefined' ? evt.bubbles : false;
        fixEvent.cancelable = typeof evt.cancelable !== 'undefined' ? evt.cancelable : true;
        fixEvent.currentTarget = sender;
        if (!fixEvent.target){ // 多次繫結統一事件,只fix一次
            fixEvent.target = fixEvent.srcElement || sender;

            fixEvent.eventPhase = fixEvent.target === sender ? 2 : 3;
            if (!fixEvent.preventDefault) {
                fixEvent.preventDefault = _preventDefault;
                fixEvent.stopPropagation = _stopPropagation;
                fixEvent.stopImmediatePropagation = _stopImmediatePropagation;
            }
            //參考:http://www.nowamagic.net/javascript/js_EventMechanismInDetail.php
            if( fixEvent.pageX == null && fixEvent.clientX != null ) {
                var doc = document.documentElement, body = document.body;
                fixEvent.pageX = fixEvent.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
                fixEvent.pageY = fixEvent.clientY + (doc && doc.scrollTop  || body && body.scrollTop  || 0) - (doc && doc.clientTop  || body && body.clientTop  || 0);
            }
            if (!fixEvent.relatedTarget && fixEvent.fromEvent) {
                fixEvent.relatedTarget = fixEvent.fromEvent === fixEvent.target ? fixEvent.toElement : fixEvent.fromElement;
            }
            // 參考: http://www.cnblogs.com/hsapphire/archive/2009/12/18/1627047.html
            if (!fixEvent.which && fixEvent.keyCode) {
                fixEvent.which = fixEvent.keyCode;
            }
        }

        return fixEvent;
    }

    function _preventDefault(){
        this.defaultPrevented = true;
        this.returnValue = false;

        this.modified = true;
    }

    function _stopPropagation(){
        this.cancelBubble = true;

        this.modified = true;
    }

    function _stopImmediatePropagation(){
        this.isStopImmediatePropagation = true;
        this.modified = true;
    }

    function _addStopImmediate(listener) {
        return function(evt) { // 除了包裝listener外,還要保證所有的事件函式共用一個evt物件
            if (!evt.isStopImmediatePropagation) {
                //evt.stopImmediatePropagation = _stopImmediateProgation;
                return listener.apply(this, arguments);
            }
        }
    }

    // 模擬冒泡事件
    var sythenticBubble = function(target, type, evt){
        var method = 'on' + type;
        var args = Array.prototype.slice.call(arguments, 2);
        // 保證使用emit觸發dom事件時,event的有效性
        if ('parentNode' in target) {
            var newEvent = args[0] = {};
            for (var p in evt) {
                newEvent[p] = evt[p];
            }

            newEvent.preventDefault = _preventDefault;
            newEvent.stopPropagation = _stopPropagation;
            newEvent.stopImmediatePropagation = _stopImmediatePropagation;
            newEvent.target = target;
            newEvent.type = type;
        }

        do{
            if (target && target[method]) {
                target[method].apply(target, args);
            }
        }while(target && (target = target.parentNode) && target[method] && newEvent && newEvent.bubbles);
    }

    var emit = function(target, type, evt){
        if (target.dispatchEvent && document.createEvent){
            var newEvent = document.createEvent('HTMLEvents');
            newEvent.initEvent(type, evt && !!evt.bubbles, evt && !!evt.cancelable);
            if (evt) {
                for (var p in evt){
                    if (!(p in newEvent)){
                        newEvent[p] = evt[p];
                    }
                }
            }

            target.dispatchEvent(newEvent);
        } /*else if (target.fireEvent) {
            target.fireEvent('on' + type);// 使用fireEvent在evt引數中設定bubbles:false無效,所以棄用
        } */else {
            return sythenticBubble.apply(on, arguments);
        }
    }

    return {
        on: on,
        emit: emit
    };
})()
</script>
<style type="text/css"></style>
</head>
<body>
  <form>
    <input type="button" value="Replace Content" >
  </form>
</body>
</html>

腦圖

歡迎各位有志之士前來交流探討!

相關文章