一、說明,、引言

我JS還是比較薄弱的,,本文的內(nèi)容屬于邊學(xué)邊想邊折騰的碎碎念,可能沒什么條理,,可能有表述不準確的地方,,可能內(nèi)容比較拗口生僻。如果您時間緊迫,,或者JS造詣已深,,至此您就可以點擊右側(cè)廣告(木有?則RSS或盜版)然后撤了,。

事件是個大課題,,真要從斷奶開始講起的話,可以寫個12期的連載,。關(guān)于JS事件的文章(類似DOM/BOM事件模型,,IE與其他瀏覽器事件差異,DOM1/DOM2事件定義等)落葉般隨處可見,。熟豆子反復(fù)炒一點意思都沒有,,因此,這里談?wù)勛约焊信d趣的自定義事件以及周邊,。

所謂自定義事件,,就是有別于有別于帶有瀏覽器特定行為的事件(類似clickmouseoversubmitkeydown等事件),事件名稱可以隨意定義,,可以通過特定的方法進行添加,,觸發(fā)以及刪除,。

二、JS自定義事件

循序漸進便于接收,。慢慢來~~

先看個簡單的事件添加的例子:

element.addEventListener("click", function() {
    // 我是臨時工
});

這是個簡單的為DOM元素分配事件處理函數(shù)的方法(IE 不支持),,有別于:

element.onclick = function() {
   // 我是臨時工 
};

addEventListener()可以為元素分配多個處理函數(shù)(而非覆蓋),因此,,我們可以繼續(xù):

element.addEventListener("click", function() {
    // 我是二代臨時工
});

然后,,當element被click(點擊)的時候,就會連續(xù)觸發(fā)“臨時工”和“二代臨時工”函數(shù),。

抽象→具象→本質(zhì)→數(shù)據(jù)層
你有沒有覺得這種行為表現(xiàn)有點類似于往長槍里面塞子彈(add),,(扣動扳手 – click)發(fā)射的時候按照塞進去的順序依次出來。這種行為表現(xiàn)為我們實現(xiàn)自定義事件提供了思路:我們可以定義一個數(shù)組,,當添加事件的時候,,我們push進去這個事件處理函數(shù);當我們執(zhí)行的時候,,從頭遍歷這個數(shù)組中的每個事件處理函數(shù),,并執(zhí)行,。

當多個事件以及對應(yīng)數(shù)據(jù)處理函數(shù)添加后,,我們最終會得到一個類似下面數(shù)據(jù)結(jié)構(gòu)的對象:

_listener = {
    "click": [func1, func2],
    "custom": [func3],
    "defined": [func4, func5, func6]
}

因此,如果我們脫離DOM, 純碎在數(shù)據(jù)層面自定義事件的話,,我們只要以構(gòu)建,、遍歷和刪除_listener對象為目的即可。

函數(shù)式實現(xiàn)
還是那句話,,循序漸進,,我們先看看函數(shù)式的實現(xiàn)(只展示骨干代碼):

var _listener = {};
var addEvent = function(type, fn) {
    // 添加
};
var fireEvent = function(type) {
    // 觸發(fā)
};
var removeEvent = function(type, fn) {
    // 刪除
};

上面的代碼雖然顯得比較初級,但是目的亦可實現(xiàn),。例如:

addEvent("alert", function() {
    alert("彈出,!");
});

// 觸發(fā)自定義alert事件
fireEvent("alert");

但是,函數(shù)式寫法缺點顯而易見,,過多暴露在外的全局變量(全局變量是魔鬼),,方法無級聯(lián)等。這也是上面懶得顯示完整代碼的原因,,略知即可,。

字面量實現(xiàn)
眾所周知,減少全局變量的方法之一就是使用全局變量(其他如閉包),。于是,,我們稍作調(diào)整(代碼較長,為限制篇幅,,使用了滾動條,,完整顯示點擊這里 – JS交互, RSS中無效果):

var Event = {
    _listeners: {},    
    // 添加
    addEvent: function(type, fn) {
        if (typeof this._listeners[type] === "undefined") {
            this._listeners[type] = [];
        }
        if (typeof fn === "function") {
            this._listeners[type].push(fn);
        }    
        return this;
    },
    // 觸發(fā)
    fireEvent: function(type) {
        var arrayEvent = this._listeners[type];
        if (arrayEvent instanceof Array) {
            for (var i=0, length=arrayEvent.length; i<length; i+=1) {
                if (typeof arrayEvent[i] === "function") {
                    arrayEvent[i]({ type: type });    
                }
            }
        }    
        return this;
    },
    // 刪除
    removeEvent: function(type, fn) {
    	var arrayEvent = this._listeners[type];
        if (typeof type === "string" && arrayEvent instanceof Array) {
            if (typeof fn === "function") {
                // 清除當前type類型事件下對應(yīng)fn方法
                for (var i=0, length=arrayEvent.length; i<length; i+=1){
                    if (arrayEvent[i] === fn){
                        this._listeners[type].splice(i, 1);
                        break;
                    }
                }
            } else {
                // 如果僅僅參數(shù)type, 或參數(shù)fn邪魔外道,,則所有type類型事件清除
                delete this._listeners[type];
            }
        }
        return this;
    }
};

使用類似下面:

Event.addEvent("alert", function() {
    alert("彈出!");
});

// 觸發(fā)自定義alert事件
Event.fireEvent("alert");

您可以狠狠地點擊這里:JS自定義事件字面量書寫demo

默認頁面document通過Event.addEvent()綁定了兩個自定義的alert事件,,因此,,此時您點擊頁面的空白區(qū)域(非按鈕與示例代碼區(qū)域),就會有如下圖所示的連續(xù)兩個alert框:

 

demo頁面還有兩個按鈕,,用來清除已經(jīng)綁定的alert事件,。第一個按鈕清除所有alert事件,而點擊第二個按鈕清除第一個alert事件,。例如我們點擊第二個按鈕:

清除完畢后再點擊頁面的空白區(qū)域,, 您會發(fā)現(xiàn)只會彈出“第二個彈出!”字樣的彈出框了,。這表明,,第一個綁定自定義事件被remove掉了。

字面量實現(xiàn)雖然減少了全局變量,,但是其屬性方法等都是暴露而且都是唯一的,,一旦某個關(guān)鍵屬性(如_listeners)不小心在某事件處reset了下,則整個全局的自定義事件都會崩潰,。因此,,我們可以進一步改進,例如,,使用原型鏈繼承,,讓繼承的屬性(如_listeners)即使出問題也不會影響全局。

原型模式實現(xiàn)
代碼如下(相比上面增加了addEventsfireEventsremoveEvents多事件綁定,、執(zhí)行與刪除方法,,篇幅較長,增加滾動限高,,點擊這里完整展示 – JS交互, RSS中無效果)(一堆代碼看得頭大,,建議直接跳過):

var EventTarget = function() {
    this._listener = {};
};

EventTarget.prototype = {
    constructor: this,
    addEvent: function(type, fn) {
        if (typeof type === "string" && typeof fn === "function") {
            if (typeof this._listener[type] === "undefined") {
                this._listener[type] = [fn];
            } else {
                this._listener[type].push(fn);    
            }
        }
        return this;
    },
    addEvents: function(obj) {
        obj = typeof obj === "object"? obj : {};
        var type;
        for (type in obj) {
            if ( type && typeof obj[type] === "function") {
                this.addEvent(type, obj[type]);    
            }
        }
        return this;
    },
    fireEvent: function(type) {
        if (type && this._listener[type]) {
            var events = {
                type: type,
                target: this    
            };
            
            for (var length = this._listener[type].length, start=0; start<length; start+=1) {
                this._listener[type][start].call(this, events);
            }
        }
        return this;
    },
    fireEvents: function(array) {
        if (array instanceof Array) {
            for (var i=0, length = array.length; i<length; i+=1) {
                this.fireEvent(array[i]);
            }
        }
        return this;
    },
    removeEvent: function(type, key) {
        var listeners = this._listener[type];
        if (listeners instanceof Array) {
            if (typeof key === "function") {
                for (var i=0, length=listeners.length; i<length; i+=1){
                    if (listeners[i] === listener){
                        listeners.splice(i, 1);
                        break;
                    }
                }
            } else if (key instanceof Array) {
                for (var lis=0, lenkey = key.length; lis<lenkey; lis+=1) {
                    this.removeEvent(type, key[lenkey]);
                }
            } else {
                delete this._listener[type];
            }
        }
        return this;
    },
    removeEvents: function(params) {
        if (params instanceof Array) {
            for (var i=0, length = params.length; i<length; i+=1) {
                this.removeEvent(params[i]);
            }    
        } else if (typeof params === "object") {
            for (var type in params) {
                this.removeEvent(type, params[type]);    
            }
        }
        return this;    
    }
};

啰哩吧嗦的代碼直接跳過,其實上面代碼跟字面量方法相比,,就是增加了下面點東西:

var EventTarget = function() {
    this._listener = {};
};

EventTarget.prototype = {
    constructor: this,
    // .. 完全就是字面量模式實現(xiàn)腳本
};

然后,,需要實現(xiàn)自定義事件功能時候,先new構(gòu)造下:

var myEvents = new EventTarget();
var yourEvents = new EventTarget();

這樣,,即使myEvents的事件容器_listener跛掉,,也不會污染yourEvents中的自定義事件(_listener安然無恙)。

您可以狠狠地點擊這里:原型模式下的JS自定義事件demo

從demo右半?yún)^(qū)域的源代碼展示可以看出如何使用addEventsfireEvents方法同時添加和觸發(fā)多個自定義事件的,。

//zxx: 下面為廣告~~注意不要勿點~~嘻嘻~~

三,、DOM自定義事件

我們平常所使用的事件基本都是與DOM元素相關(guān)的,例如點擊按鈕,文本輸入等,,這些為自帶瀏覽器行為事件,,而自定義事件與這些行為無關(guān)。例如:

element.addEventListener("alert", function() {
    alert("彈出,!");
});

這里的alert就屬于自定義事件,,后面的function就是自定義事件函數(shù)。而這個自定義事件是直接綁定在名為element的DOM元素上的,,因此,,這個稱之為自定義DOM事件。

由于瀏覽器的差異,,上面的addEventListener在IE瀏覽器下混不來(attachEvent代替),,因此,為了便于規(guī)模使用,,我們需要新的添加事件方法名(合并addEventListenerattachEvent),,例如addEvent, 并附帶事件觸發(fā)方法fireEvent, 刪除事件方法removeEvent,(命名均參考自MooTools庫),。

如何直接在DOM上擴展新的事件處理方法,,以及執(zhí)行自定義的事件呢?

如果不考慮IE6/7瀏覽器,,我們可以直接在DOM上進行方法擴展,。例如添加個addEvent方法:

HTMLElement.prototype.addEvent = function(type, fn, capture) {
    var el = this;
    if (window.addEventListener) {
        el.addEventListener(type, function(e) {
            fn.call(el, e);
        }, capture);
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    } 
};

//zxx: 上面代碼中的HTMLElement表示HTML元素。以一個<p>標簽元素舉例,,其向上尋找原型對象用過會是這樣:HTMLParagraphElement.prototype →HTMLElement.prototype → Element.prototype → Node.prototype → Object.prototype → null,。這下您應(yīng)該知道HTMLElement所處的位置了吧,,上述代碼HTMLElement直接換成Element也是可以的,,但是會讓其他元素(例如文本元素)也擴展addEvent方法,有些浪費了,。

這樣,,我們就可以使用擴展的新方法給元素添加事件了,例如一個圖片元素:

elImage.addEvent("click", function() {
    alert("我是點擊圖片之后的彈出,!");
});

由于IE6, IE7瀏覽器的DOM水平較低,,無法直接進行擴展,因此,,原型擴展的方法在這兩個瀏覽器下是行不通的,。要想讓這兩個瀏覽器也支持addEvent方法,只能是頁面載入時候遍歷所有DOM,,然后每個都直接添加addEvent方法了,。

var elAll = document.all, lenAll = elAll.length;
for (var iAll=0; iAll<lenAll; iAll+=1) {
    elAll[iAll].addEvent = function(type, fn) {
        var el = this;
        el.attachEvent("on" + type, function(e) {
            fn.call(el, e);
        });
    };
}

您可以狠狠地點擊這里:基于DOM擴展自定義方法demo

點擊demo頁面張含韻小姐年輕時候相片,就會有該圖片alt屬性值,。

測試代碼如下(demo頁面有代碼完整展示):

<img id="image" src="http://image./image/study/s/s256/mm1.jpg" alt="年輕的張含韻" />

document.getElementById("image").addEvent("click", function() {
    alert("這是:" + this.alt);    
});

只能點到為止
直接在DOM上進行事件方法擴展其實是個糟糕的做法,,因此,,這里我并沒有對自定義事件做進一步深入探討(這個下一部分會講)。

基于DOM擴展缺點有:缺少標準無規(guī)律,、提高沖突可能性,、性能以及瀏覽器支持。
擴展名字任意命,,很有可能就會與未來DOM瀏覽器本身支持的方法相互沖突,;擴展無規(guī)律,很有可能出現(xiàn)A和B同名不同功能的擴展而造成沖突,;IE6-7瀏覽器下所有擴展都要通過遍歷支持,,其性能開銷可想而知;另外IE8對DOM擴展的支持并不完整,,例如其支持Element.prototype,,卻沒有HTMLElement.prototype.

雖然我從事的站點就是基于MooTools庫的,但是,,我對MooTools庫基于DOM擴展方法的做法是不支持的,。相反,我更親近jQuery庫的做法,,也就是下面要講的“偽DOM自定義事件”,。

四、偽DOM自定義事件

這里的“偽DOM自定義事件”是自己定義的一個名詞,,用來區(qū)分DOM自定義事件的,。例如jQuery庫,其是基于包裝器(一個包含DOM元素的中間層)擴展事件的,,既與DOM相關(guān),,又不直接是DOM,因此,,稱之為“偽DOM自定義事件”,。

//zxx: 下面即將展示的代碼目的在于學(xué)習與認識,要想實際應(yīng)用可能還需要在細節(jié)上做些調(diào)整,。例如,,下面測試的包裝器僅僅只是包裹DOM元素,并非選擇器之類,;$符號未增加沖突處理,,且?guī)讉€重要方法都暴露在全局環(huán)境中,沒有閉包保護等,。

原型以及new函數(shù)構(gòu)造不是本文重點,,因此,下面這個僅展示:

var $ = function(el) {
    return new _$(el);    
};
var _$ = function(el) {
    this.el = el;
};
_$.prototype = {
    constructor: this,
    addEvent: function() {
        // ...
    },
    fireEvent: function() {
        // ...
    },
    removeEvent: function() {
        // ...
    }
}

于是我們就可以使用類似$(dom).addEvent()的語法為元素添加事件了(包括不包含瀏覽器行為的自定義事件)。

自定義事件的添加
如果只考慮事件添加,,我們的工作其實很簡單,,根據(jù)支持情況,addEventListenerattachEvent方法分別添加事件(attachEvent方法后添加事件先觸發(fā))即可:

addEvent: function(type, fn, capture) {
    var el = this.el;
    if (window.addEventListener) {
        el.addEventListener(type, fn, capture);        
    } else if (window.attachEvent) {
        el.attachEvent("on" + type, fn);
    }
    return this;
}

顯然,,事情不會這么簡單,,有句古話叫做“上山容易下山難”,自定義事件添加容易,,但是如何觸發(fā)它們呢,?——考慮到自定義事件與瀏覽器行為無關(guān),同時瀏覽器沒有直接的觸發(fā)事件的方法,。

自定義事件的觸發(fā)
又是不可避免的,,由于瀏覽器兼容性問題,我們要分開說了,,針對標準瀏覽器和IE6/7等考古瀏覽器,。

1. 對于標準瀏覽器,其提供了可供元素觸發(fā)的方法:element.dispatchEvent(). 不過,,在使用該方法之前,,我們還需要做其他兩件事,及創(chuàng)建和初始化,。因此,,總結(jié)說來就是:

document.createEvent()
event.initEvent()
element.dispatchEvent()

舉個板栗:

$(dom).addEvent("alert", function() {
    alert("彈彈彈,彈走魚尾紋~~");
});

// 創(chuàng)建
var evt = document.createEvent("HTMLEvents");
// 初始化
evt.initEvent("alert", false, false);

// 觸發(fā), 即彈出文字
dom.dispatchEvent(evt);

createEvent()方法返回新創(chuàng)建的Event對象,,支持一個參數(shù),,表示事件類型,具體見下表:

參數(shù)事件接口初始化方法
HTMLEventsHTMLEventinitEvent()
MouseEventsMouseEventinitMouseEvent()
UIEventsUIEventinitUIEvent()

關(guān)于createEvent()方法我自己了解也不是很深入,,不想濫竽充數(shù),,誤人子弟,所以您有疑問我可能作答不了,,希望對熟知該方法的人可以做進一步的解釋說明(例如事件接口與document關(guān)系,,UIEvent是什么東西等),。

initEvent()方法用于初始化通過DocumentEvent接口創(chuàng)建的Event的值,。支持三個參數(shù):initEvent(eventName, canBubble, preventDefault). 分別表示事件名稱,是否可以冒泡,,是否阻止事件的默認操作,。

dispatchEvent()就是觸發(fā)執(zhí)行了,dom.dispatchEvent(eventObject), 參數(shù)eventObject表示事件對象,,是createEvent()方法返回的創(chuàng)建的Event對象,。

2. 對于IE瀏覽器,由于向下很多版本的瀏覽器都不支持document.createEvent()方法,因此我們需要另辟蹊徑(據(jù)說IE有document.createEventObject()event.fireEvent()方法,,但是不支持自定義事件~~),。

IE瀏覽器有不少自給自足的東西,例如下面要說的這個"propertychange"事件,,顧名思意,,就是屬性改變即觸發(fā)的事件。例如文本框value值改變,,或是元素id改變,,或是綁定的事件改變等等。

我們可以利用這個IE私有的東西實現(xiàn)自定義事件的觸發(fā),,大家可以先花幾分鐘想想……

// zxx: 假設(shè)幾分鐘已經(jīng)過去了……

大家現(xiàn)在有思路了沒,?其實說穿了很簡單,當我們添加自定義事件的時候,,順便給元素添加一個自定義屬性即可,。例如,我們添加自定義名為"alert"的自定義事件,,順便我們可以對元素做點小手腳:

dom.evtAlert = "2012-04-01";

再順便把自定義事件fn塞到"propertychange"事件中:

dom.attachEvent("onpropertychange", function(e) {
    if (e.propertyName == "evtAlert") {
        fn.call(this);
    }
});

這個,,當我們需要觸發(fā)自定義事件的時候,只要修改DOM上自定義的evtAlert屬性的值即可:

dom.evtAlert = Math.random();	// 值變成隨機數(shù)

此時就會觸發(fā)dom上綁定的onpropertychange事件,,又因為修改的屬性名正好是"evtAlert", 于是自定義的fn就會被執(zhí)行,。這就是IE瀏覽器下事件觸發(fā)實現(xiàn)的完整機制,應(yīng)該說講得還是蠻細的,。

自定義事件的刪除
與觸發(fā)事件不同,,事件刪除,各個瀏覽器都提供了對于的時間刪除方法,,如removeEventListenerdetachEvent,。不過呢,對于IE瀏覽器,,還要多刪除一個事件,,就是為了實現(xiàn)觸發(fā)功能額外增加的onpropertychange事件:

dom.detachEvent("onpropertychange", evt);

大綜合
結(jié)合上面所有論述與展示,我們可以得到類似下面的完整代碼(為限制篇幅,,滾動定高,,想查看完整代碼推薦去原demo,或是點擊這里完整顯示– js交互,,RSS中無效果,。):

var $ = function(el) {
    return new _$(el);    
};
var _$ = function(el) {
    this.el = (el && el.nodeType == 1)? el: document;
};
_$.prototype = {
    constructor: this,
    addEvent: function(type, fn, capture) {
        var el = this.el;
        if (window.addEventListener) {
            el.addEventListener(type, fn, capture);
            var ev = document.createEvent("HTMLEvents");
            ev.initEvent(type, capture || false, false);
            
            if (!el["ev" + type]) {
                el["ev" + type] = ev;
            }  
        } else if (window.attachEvent) {
            el.attachEvent("on" + type, fn);    
            if (isNaN(el["cu" + type])) {
                // 自定義屬性
                el["cu" + type] = 0; 
            }   
            var fnEv = function(event) {
                if (event.propertyName == "cu" + type) { fn.call(el); }
            };
            el.attachEvent("onpropertychange", fnEv);     
            if (!el["ev" + type]) {
                el["ev" + type] = [fnEv];
            } else {
                el["ev" + type].push(fnEv);    
            }
        }
        return this;
    },
    fireEvent: function(type) {
        var el = this.el;
        if (typeof type === "string") {
            if (document.dispatchEvent) {
                if (el["ev" + type]) {
                    el.dispatchEvent(el["ev" + type]);
                }
            } else if (document.attachEvent) {
                el["cu" + type]++;
            }    
        }    
        return this;
    },
    removeEvent: function(type, fn, capture) {
        var el = this.el;
        if (window.removeEventListener) {
            el.removeEventListener(type, fn, capture || false);
        } else if (document.attachEvent) {
            el.detachEvent("on" + type, fn);
            var arrEv = el["ev" + type];
            if (arrEv instanceof Array) {
                for (var i=0; i<arrEv.length; i+=1) {
                    el.detachEvent("onpropertychange", arrEv[i]);
                }
            }
        }
        return this;    
    }
};

您可以狠狠地點擊這里:JS DOM自定義事件demo

demo頁面中的的張含韻小姐圖片上通過級聯(lián)形式聯(lián)系添加了三個事件(一個是包含瀏覽器行為的click事件,還有兩個是自定義不含行為的alert事件):

$(elImage)
    .addEvent("click", funClick);
    .addEvent("alert", funAlert1)
    .addEvent("alert", funAlert2);

funClick方法中有等同下面腳本:

$(e.target).fireEvent("alert");

因此,,點擊圖片,,才會出現(xiàn)三個彈出框:用戶點擊圖片 → 執(zhí)行funClick → 第一個彈框 → 執(zhí)行fireEvent → 觸發(fā)自定義"alert"事件 → 連續(xù)兩個"alert"事件彈框

當點擊圖片下面的按鈕清除掉自定義"alert"事件后,,再點擊圖片就只有一個彈出咯(funAlert1funAlert2提前回家掃墓去了)!

 

五,、清明節(jié)前的結(jié)語

鑫表情

明天回家,,很顯然,我要釣魚釣死在河邊上,。

時間等客觀原因,,本文展示的些腳本并未做非常詳盡嚴謹?shù)臏y試,因此,,不建議直接Copy到實際項目中應(yīng)用,,更多旨在相互交流與學(xué)習。例如在IE瀏覽器下,,最后的“偽DOM自定義事件”,,click事件通過點擊觸發(fā)時的事件類型(event.type)是click, 但是通過fireEvent觸發(fā)的時候事件類型是propertychange, 這些細節(jié)在測試學(xué)習的時候都是可以忽略的,但是要是實際應(yīng)用,,這都是需要完善的,。

本想以很通俗易懂的語言闡述我想表達的內(nèi)容,但是,,現(xiàn)在回過頭看看,,做得并不好,術(shù)語,,啰嗦的話語還是顯得多了點,,這方面的功力還需要加強,或許是本身理解不透徹的緣故,,無法駕馭自然無法語言通俗化,。

雖說自己JS方面的學(xué)習比兩年前要好多了(那個時候連addEventListenerattachEvent放在一起干嘛的都不清楚),但是心里清楚的很,,JS還是很薄弱的,,跟真正優(yōu)秀的JS開發(fā)人員相比,要積累的還有很多,。什么時候能夠像看有色小說一樣把《JavaScript語言精粹》一書讀下來,,恩,估計可以有臉得瑟得瑟了~~

本文涉及的一些知識點歡迎補充提點,,有表述不準確的地方歡迎指正,。

最后,祝大家清明節(jié)快樂,!額,?怎么這句話怪怪的——上墳一般快樂不起來吧~~那大家祝我清明回家釣魚大豐收,大爆箱??!哈哈!,!

原創(chuàng)文章,,轉(zhuǎn)載請注明來自張鑫旭-鑫空間-鑫生活[http://www.]
本文地址:http://www./wordpress/?p=2330