Javascript事件機制兼容性解決方案

  本文的解決方案能夠用於Javascript native對象和宿主對象(dom元素),經過如下的方式來綁定和觸發事件:javascript

  

 

  或者php

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');
View Code

 

After函數css

  

  爲native對象添加事件的過程主要在after函數中完成,這個函數主要作了如下幾件事:html

  1. 若是obj中已有響應函數,將其替換成dispatcher函數
  2. 使用鏈式結構,保證屢次綁定事件函數的順序執行
  3. 返回一個handle對象,調用remove方法能夠去除本次事件綁定

  下圖爲after函數調用先後onlog函數的引用前端

    

 

      (調用前)java

  

      (調用後)程序員

  詳細解釋請看註釋,但願讀者可以跟着運行一遍chrome

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;
}
View Code

  

解決兼容性編程

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

  1. 屢次綁定一個事件,事件處理函數的調用順序問題
  2. 事件處理函數中的this關鍵字指向問題
  3. 標準化event事件對象,支持經常使用的事件屬性

 

  因爲使用attachEvent方法添加事件處理函數沒法保證事件處理函數的調用順序,因此咱們棄用attachEvent,轉而用上文中的after生成的正序鏈式結構來解決這個問題。

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

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

  

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

//一、統一事件觸發順序
    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;
        }
    }
View Code

  對於事件對象的標準化,咱們須要將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;
    }
View Code

  _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);
        }
    }
View Code

  附上完整代碼:

<!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;
}

//一、統一事件觸發順序
//二、標準化事件對象
//三、模擬冒泡 emit時保持冒泡行爲,注意input.onclick這種方式是不冒泡的
//四、保持冒泡過程當中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
    //一、統一事件觸發順序
    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>
View Code

  腦圖

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

參考文章:

【前端盲點】事件的幾個階段你真的瞭解麼???

JavaScript事件機制詳細研究 

[解惑]JavaScript事件機制

 javascript線程解釋(setTimeout,setInterval你不知道的事)

相關文章
相關標籤/搜索