前端事件系統(一)


事件是前端之中,非常重要的一個部分。其作用在於對於用戶的各種行為進行相應。近日打算對於事件系統進行更為深入的學習,同時,對於這一部分學習的內容進行一個總結。因為瀏覽器發展至今,事件系統本身已經尤為的復雜了,所以事件這一部分內容可能會將分為很多章來進行總結。本章將對於事件系統,根據個人的經驗,以及其他地方學到的東西進行一個歸納,給出一些簡單地處理方案,而在后面幾章將會引入經典jquery的源碼進行閱讀。

綁定方式整理

事件系統發展至今,我們常見的對於事件的綁定方式有三種。

  1. 直接將其寫在元素標簽之中。類似用如下的寫法

     <div onClick="function">
    

    這種方法可以說是非常古老的寫法了,不過至今還是會有人在用。對已現在來說其實並不推薦使用這種方法來綁定事件,其不推薦使用的原因將在第二種綁定方法之中說明。

  2. 對於第一種onXXX的方法,也采用如下的方法進行綁定

     el.onclick = function
    

    這種方法其實和第一種綁定方法本質上是一樣的。也就是我們常常所說的dom0事件。之前在一種中也說過,其實現在並不推薦使用這種方法,原因如下

    1. 該方法綁定的事件所執行的回調函數只允許一個,倘若綁定了兩個,那么第二個將覆蓋掉對於第一個的綁定。
    2. 該方式只支持事件冒泡
    3. 在ie下的該方式的回調函數,並不能像我們往常一樣,擁有事件對象參數。
    4. 該方式對於dom3的部分新增事件不支持。同時對於FF的部分私有實現也並不支持。

    其實對於該方式綁定事件來說,前面三點就決定了,我們不能使用該方法進行綁定事件。而對於第四點,dom3的部分新增事件的不支持,其實我們在日常的使用之中,對於不支持的那些事件,我們的使用頻率也是很低的。因為許多新事件,可能還未曾進入我們的視野,就已經被廢棄了。

  3. 最后就是我們常說的dom2事件系統了

    不過dom2事件系統,現存的擁有兩套不同的API,因此,在下面將分為兩方。

    ie方面,對於事件的綁定,采用如下的方法

     el.attachEvent("on"+type,callback)
    

    這是微軟對於ie5添加的API(除了事件綁定外,還有相應的解綁,創建,派發等)。他解決了之前采用onXXX方法會導致的只允許一個回調的情況,支持了對於同種事件多個回調的綁定。但是這套方案,其實並沒有給前端帶來什么好處,當你對一個事件系統進行處理的適合,應該能很深的感覺到這種方式其帶來的無數問題,以及對於這些問題的解決,會花費很多很多的心思。大致帶來的問題如下

    • 對於dom3事件的不支持
    • this的指向不是被綁定元素,而是(個人感覺這也是this指向極為特殊的一個情況) - 對於多個回調的綁定,其執行順序卻並不是按照理所當然的想的那樣按照綁定順序來執行,而是按照不規律的順序來執行的。
    • 其event事件對象與w3c的event對象存在極大差異
    • 同樣只支持事件冒泡

    w3c方面,對於事件的綁定,采用如下方案

     el.addEventListener(type,callback,[phase])
    

    這個是我們現代瀏覽器上使用的方法,ie9開始也對於這套API進行了支持,這應該是我們目前最常為使用的方案,當然這套方法也擁有他的一些問題。

    • 像之前所提到的,新事件本身就是不穩定的。可能還沒有進入人們的視野,就已經被廢棄掉了
    • 許多瀏覽器並不遵循w3c的標准,對於一些事件並不予以支持
    • 惡心的前綴標識部分存在於事件名上
    • 因為w3c的標准制定,晚於一些瀏覽器廠商,因此,對於早期一些版本的瀏覽器,事件的對象成員同樣存在和w3c標准差異的情況

事件系統的處理

事件系統是前端之中,極為核心的一個部分,因此,我們必須對其種種問題進行一個個的處理。先拋開強大的jquery的事件處理,倘若我們要寫出一個對於事件系統的處理,並將其投入使用,那么我們至少應當解決如下的幾個問題。

  1. 不同瀏覽器對於事件系統的API支持的問題
  2. IE的this指向問題
  3. 事件對象的差異性問題
  4. IE執行回調的順序問題

那么,既然整理好了問題,我們現在就可以開始去解決那些問題。

對於不同瀏覽器API的支持問題:

我們采用條件判斷來進行簡單地實現就好

function addEvent(target,eventName,callback,useCapture){

    if(target.addEventListener){    //w3c方法優先
        target.addEventListener(eventName,callback,useCapture);
    }else if(target.attachEvent){   //然后采用ie下方法
        target.attachEvent("on"+eventName,callback);
    }else{      //最后在考慮使用onXXX形式
        target["on"+eventName] = callback;
    }

    //返回回調函數,方便用於事件解綁
    return callback;

}

function removeEvent(target,eventName,callback,useCapture) {

    if (target.removeEventListener) {
        target.removeEventListener(eventName, callback, useCapture);
    } else if (target.detachEvent) {
        target.detachEvent("on" + eventName, callback);
    } else {      //onXXX的形式直接將其設置為null即可
        target["on" + eventName] = null;
    }

}

這樣,對於問題1,可以說算是解決了,而這樣的一個事件注冊函數,對於對事件系統簡易需求的頁面,已經很是足夠了。不過既然提出了那些問題,那么就一一來進行解決吧。

對於ie下this指向的問題:

說點題外話,關於this的指向,其實很簡單的來說,就是是誰調用的,this就指向誰。比較籠統的總結一下,日常this的指向就兩種情況,對象中的this,那么就指向其對應的對象,普通函數中的this,就指向window。然而attachEvent的this指向,卻是指向window的,因此,我們不得不對其進行修改。實現方式很簡單,使用call或者apply,對於this指向進行修改就可以了。因此,上面的代碼可以修改如下。

function addEvent(target,eventName,callback,useCapture){

    if(target.addEventListener){    //w3c方法優先
        target.addEventListener(eventName,handler,useCapture);
    }else if(target.attachEvent){   //然后采用ie下方法
        target.attachEvent("on"+eventName,handler);
    }else{      //最后在考慮使用onXXX形式
        target["on"+eventName] = handler;
    }

    //增加handler函數,在其中對於this指向進行改變,同時,采用handler處理函數來進行事件回調
    function handler(){
        callback.call(target);
    }

    //返回回調函數,方便用於事件解綁
    return handler;

}

function removeEvent(target,eventName,callback,useCapture) {

    if (target.removeEventListener) {
        target.removeEventListener(eventName, callback, useCapture);
    } else if (target.detachEvent) {
        target.detachEvent("on" + eventName, callback);
    } else {      //onXXX的形式直接將其設置為null即可
        target["on" + eventName] = null;
    }

}

事件對象的差異性問題:

event對象以及對其的處理也是事件綁定之中,要進行的一個重點內容。而具體的處理,我們將在之前對於this指向處理之中的handler中來一一進行解決。

大致就是對於target,currentTarget,冒泡,取消默認事件這幾部分來進行簡單地處理。修改后的代碼如下

function addEvent(target,eventName,callback,useCapture){

    if(target.addEventListener){    //w3c方法優先
        target.addEventListener(eventName,handler,useCapture);
    }else if(target.attachEvent){   //然后采用ie下方法
        target.attachEvent("on"+eventName,handler);
    }else{      //最后在考慮使用onXXX形式
        target["on"+eventName] = handler;
    }

    //處理傳入的參數ev
    function handler(event){
        //ie下的事件名需要window.event
        var ev = event || window.event,
            stopPropagation = ev.stopPropagation,
            preventDefault = ev.preventDefault;

        //獲取觸發事件的對象 ie下的ev.srcElement相當於其他瀏覽器下ev.target
        ev.target = ev.target || ev.srcElement;
        //獲取當前事件活動的對象(捕獲或者冒泡階段)
        ev.currentTarget = ev.currentTarget || target;
        //取消冒泡的處理
        ev.stopPropagation = function(){
            if(stopPropagation){
                stopPropagation.call(event);
            }else{
                ev.cancelBubble = true;
            }
        };
        //取消默認事件的處理
        ev.preventDefault = function(){
            if(preventDefault){
                preventDefault.call(event);
            }else{
                ev.returnValue = false;
            }
        };

        //執行callback函數,並且this指向,同時用flag接收其返回值
        var flag = callback.call(target,ev);

        //處理flag接收到的返回着為false的情況
        if(flag === false){
            ev.stopPropagation();
            ev.preventDefault();
        }
    }

    //返回回調函數,方便用於事件解綁
    return handler;

}

補充對於匿名函數取綁的問題:

之前采用了return回調函數的方法,同時,在綁定函數時,用一個變量來存儲回調函數,在解綁時再將變量傳入,以達到解綁的目的。

我們來看一段如下的代碼

var a = addEvent(el,"click", function(){});
removeEvent(el,"click",a);

這種方法,其實對於解綁來說,代碼量是很少的。同時,也不需要在解綁的時候,再對代碼進行修改,將匿名函數變成非匿名,然后在進行操作。當然,這種方法也有些缺陷,匿名函數並不會占用命名,而這種方案,始終是需要對變量名進行占用。因此,如果執意於要對於匿名函數進行解綁的話,可以考慮對匿名函數變為非匿名的轉換。參考代碼如下

//事件注冊
function addEvent(target,eventName,callback,useCapture){

    //壓縮函數的空格
    var fnStr = callback.toString().replace(/\s+/g,'');

    if(!target[eventName+"event"]){
        target[eventName+"event"] = {};
    }

    //存儲事件的函數到target[eventName+'event'][fnStr]中
    target[eventName+"event"][fnStr] = handler;

    useCapture = useCapture || false;

    //高設上的事件注冊簡單兼容
    if(target.addEventListener){
        target.addEventListener(eventName,handler,useCapture);
    }else if(target.attachEvent){
        target.attachEvent("on"+eventName,handler);
    }else{
        target["on"+eventName] = handler;
    }

    //處理傳入的參數ev
    function handler(event){
        //ie下的事件名需要window.event
        var ev = event || window.event,
                stopPropagation = ev.stopPropagation,
                preventDefault = ev.preventDefault;

        //獲取觸發事件的對象 ie下的ev.srcElement相當於其他瀏覽器下ev.target
        ev.target = ev.target || ev.srcElement;
        //獲取當前事件活動的對象(捕獲或者冒泡階段)
        ev.currentTarget = ev.currentTarget || target;
        //取消冒泡的處理
        ev.stopPropagation = function(){
            if(stopPropagation){
                stopPropagation.call(event);
            }else{
                ev.cancelBubble = true;
            }
        };
        //取消默認事件的處理
        ev.preventDefault = function(){
            if(preventDefault){
                preventDefault.call(event);
            }else{
                ev.returnValue = false;
            }
        };

        //執行callback函數,並且this指向,同時用flag接收其返回值
        var flag = callback.call(target,ev);

        //處理flag接收到的返回着為false的情況
        if(flag === false){
            ev.stopPropagation();
            ev.preventDefault();
        }
    }
}

//事件取綁(匿名函數)
function removeEvent(target,eventName,callback,useCapture){
    //壓縮空格
    var fnStr = callback.toString().replace(/\s+/g,''),
            handler;

    if(!target[eventName+"event"]){
        return;
    }

    //獲取到存儲的函數
    handler = target[eventName+"event"][fnStr];
    useCapture = useCapture || false;

    if(target.removeEventListener){
        target.removeEventListener(eventName,handler,useCapture);
    }else if(target.detachEvent){
        target.detachEvent("on"+eventName,handler);
    }else{
        target["on"+eventName] = null;
    }
}

對於ie下執行順序的問題:

很多情況下,我們對於事件綁定的順序肯定是有要求的,因此不按照順序的執行很多時候是我們所不想看到的,因此,我們需要對於回調的執行順序進行一個處理。處理的思路也不算復雜。倘若在ie下,我們對於同一個事件做了多個回調,那么我們將對其進行判斷,並將其合並到一個回調之中。簡單來說,就是如下的這種思路

//對於el綁定了a和b兩個回調
el.attachEvent("onclick",a)
el.attachEvent("onclick",b)

//對兩個回調進行整合處理,然后讓其按順序執行
el.attachEvent("onclick",function(){
	a();
	b();
})

上面是對於思路的一種抽象,不過當真正開始具體執行的時候,其實是很復雜的。

簡單來說,這種執行方案,就是將多個函數進行打包,然后丟到一個事件綁定中去執行,而如果采用addEventListener或者attachEvent直接進行綁定的話,無論如何處理,都很難達到只對事件綁定一次的目的。(起碼用這兩個函數,采取直接對元素進行綁定,我沒想到什么很好的解決方案。)當然,這也並不是不能解決的,很早之前Dean Edwards大神的addEvent庫就巧妙的解決了這個問題,他並沒有采用流行的addeventListener/attachEvent方法,而是直接采用dom0事件系統對其進行了處理,巧妙的利用了dom0事件系統只能綁定一個事件的特性。現在很流行的jquery事件系統,很多思想也是參考的這個庫中的思想。

因此對於執行順序的處理,在上面的事件注冊之中,並沒有提及到。但是在之后的章節會進行提及。同時,這樣的事件注冊,對於需求不算復雜的頁面,也算是足夠了。

第一章也就到這里了……


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM