ExtJS框架基礎:事件模型及其常用功能

Liam Wang發表於2013-07-06

前言

工作中用ExtJS有一段時間了,Ext豐富的UI元件大大的提高了開發B/S應用的效率。雖然近期工作中天天都用到ExtJS,但很少對ExtJS框架原理性的東西進行過深入學習,這兩天花了些時間學習了下。我並不推薦大家去研究ExtJS框架的原始碼,雖然可以學習其中的思想和原理,但太浪費精力了,除非你要自己寫框架。

對於ExtJS這種框架,非遇到“雜症”的時候我覺得也沒必要去研究其原始碼和底層的原理,對其一些機制大致有個概念,懂得怎麼用就行,這也是本篇博文的主要目的。

Ext自己的事件機制

Ext中的事件遵循樹狀模型,和事件相關的類主要有這麼幾個:Ext.util.Observable、Ext.lib.Event、Ext.EventManager和Ext.EventObject。

Ext使用Ext.lib.Event、Ext.EventManager和Ext.EventObject對原生瀏覽器事件進行了封裝,最後給我們用的是一套統一的跨瀏覽器的通用事件介面。HTML元素本身已經支援事件,為什麼基本上所有的主流JS框架都要實現自己的事件機制呢?一個最主要的原因是HTML元素對事件的處理是通過簡單的單一繫結實現的,如果不進行封裝,事件只能繫結到一個事件處理控制程式碼。如下面程式碼所示:

var e = document.getElementById("test");
e.onclick = function() { alert("handler1") };
e.onclick = function() { alert("handler2") };

單擊test按鈕後會發現只會彈出一個顯示"handler2"的提示框,因為第一個被覆蓋。而使用像Ext、jQuery這樣的框架就不用擔心這個問題,同一個事件可以依次繫結多個事件處理控制程式碼,如下程式碼所示:

Ext.onReady(function () {
    var test = Ext.get("test");
    test.on("click", function () {
        alert("handler1");
    });
    test.on("click", function () {
        alert("handler2");
    });
});

Ext實現自己的事件機制,原因很多,比如為了相容不同瀏覽器之間的差異等。Ext對原生瀏覽器事件的封裝都在上面所說的幾個類中,如果在專案中要熟練應用Ext,是非常有必要了解一下和事件相關的類和常用函式的。下面開始介紹這些類和它們的功能。

Ext.util.Observable

Ext.util.Observable在Ext事件模型中有著舉足輕重的地位,位於Ext元件的頂端,為Ext元件提供處理事件的最基本的功能。所有繼承自Ext.util.Observable類的控制元件都可以支援事件。可以為這些繼承了Ext.util.Observable的物件定義一些事件,然後為這此事件配置監聽器。當某個事件觸發時,Ext會自動呼叫對應的監聽器,這些就是Ext的事件模型。
下面通過繼承Ext.util.Observable來實現一個支援事件的物件:
Ext.onReady(function () {
    //定義一個Person類。
    function Person(name) {
        this.name = name;
        this.addEvents("walk", "eat");
        this.superclass.constructor.call(this);
    }

    //1、讓Person繼承Ext.util.Observable的所有屬性,
    //   這樣Person類構造器中的addEvents和Person.superclass.constructor.call()在例項建立時才會起作用。
    //   Person的例項就可以應用Ext的事件相關的on、un等方法和在Person類構造器中的addEvents和Person.superclass.constructor.call()了。
    //2、新增一個info()函式,讓它返回Person資訊。
    Ext.extend(Person, Ext.util.Observable, {
        info: function (event) {
            return this.name + " is " + (event ? "ing" : "doing nothing") + ".";
        }
    });

    //1、建立一個Person例項,然後為它的事件配置好監聽器。
    //2、on是addListener的簡寫,un是removeListener簡寫
    var person = new Person("Liam");
    person.on("walk", function () {
        this.state = "walk";
        Ext.Msg.alert("event", this.name + " is walking.");
    });
    person.on("eat", function (meal) {
        this.state = "eat";
        Ext.Msg.alert("event", this.name + " is eating " + meal + ".");
    });

    //測試效果
    Ext.get("btnWalk").on("click", function () {
        person.fireEvent("walk");
    });
    Ext.get("btnEat").on("click", function () {
        person.fireEvent("eat", "breakfast");
    });
    Ext.get("btnInfo").on("click", function () {
        Ext.Msg.alert("info", person.info(person.state));
    });
});

以上程式碼展示了在Ext中如何通過繼承Ext.util.Observable給一個類自定義事件,到這,我們大概也瞭解了addListener/on、addEvents和fireEvent這些函式的基本用法,removeListener/un函式相關內容還會在本文後面介紹。如果要了解Ext.util.Observable的其他細節,可看看Ext官方API文件的介紹。

Ext.lib.Event

Ext.lib.Event是一個工具類,它封裝了不同瀏覽器的事件處理函式,為上層元件提供了統一功能介面。
對於這個工具類,Ext自帶的文件中沒有關於這個類的說明,實際中也很少直接用到這個類,只是與事件相關的那些操作最後都會歸結為對這些底層函式的呼叫。
Ext.lib.Event中定義了以下幾個主要函式。

getX()、getY()、getXY(),獲得發生的事件在頁面中的座標位置:

Ext.get("test").on("click", function () {
    alert(this.getX() + "," + this.getY());
});

getTarget(),返回事件的目標元素,該函式用來統一IE和其他瀏覽器使用的e.target和e.srcElement:

Ext.get("test").on("click", function (e) {
    var test = e.getTarget();
    alert(test.value);
});

on()和un(),這兩個函式就不用多說了。

preventDefault(),用於取消瀏覽器當前事件所執行的預設操作,比如阻止頁面跳轉。使用這個函式,我是不是可以阻止彈出瀏覽器滑鼠右鍵選單呢?我用下面的程式碼試了下,結果右鍵選單並沒有被阻止,誰能告訴我為什麼?

//滑鼠右鍵事件沒有被阻止?
Ext.getDoc().on("mousedown ", function (e) {
    if (e.button == "2")
        e.preventDefault();
});

stopPropagation(),停止事件傳遞。比如divTest元素訂閱了click事件,它的子元素btnTest被click時,父元素divTest的click事件也會被觸發,stopPropagation()就是用來阻止這種事件冒泡的發生:

Ext.get("divTest").on("click", function () {
    alert("divTest clicked!");
});
Ext.get("btnTest").on("click", function (e) {
    alert("btnTest clicked!");
    //阻止事件冒泡
    e.stopPropagation();
});

 stopEvent(),停止一個事件,相當於呼叫preventDefault()和stopPropagation()兩個函式。

另外還有一些幾乎用不上的函式onAvailable()、getRelatedTarget()等,就不再一一介紹了。

再次說明一下,Ext.lib.Event這個類實際中很少直接用到,用的只是上面講的一些底層通用函式,並供一些其它和事件相關的類如Ext.EventManager和Ext.EventObject的底層的呼叫。

Ext.EventManager

Ext.EventManager,作為事件管理器,定義了一系列事件相關的處理函式。其中最常用的就是onDocumentReady和onWindowResize了。

我們常用的Ext.onReady()就是Ext.EventManager.onDocumentReady()的簡寫形式,它會在頁面文件渲染完畢但圖片等資原始檔還未下載時呼叫啟動函式。

這裡有必要提一下眾所周知人人共憤的window.onresize事件:

function resizeProcess(width, height) {
    var p = document.createElement("p");
    p.innerText ="時間:" + new Date().toLocaleTimeString() + ", 寬:" + width + ", 高:" + height;
    document.body.appendChild(p);
}
//原生瀏覽器resize事件
window.onresize = function () {
    resizeProcess(document.documentElement.clientWidth, document.documentElement.clientWidth);
}

當為window.onresize新增了事件處理函式resizeProcess後,會發現resizeProcess會被執行多次,尤其是IE6、IE7、IE8,還會出現假死,動不動就崩掉。

如圖,IE8瀏覽器會直接死掉。真心深惡通絕IE6、IE7、IE8,要是有朝一日能因為IE11的出現,IE6到IE10都被消滅,那該是多麼大快人心的事!
window.onresize事件處理函式被多次乃至無數次觸發的問題,網上有不少解決方案,但稍微理想點的方案用起來都挺麻煩。Ext.EventManager下的onWindowResize事件處理函式就非常好的解決了這個問題:

Ext.onReady(function () {
    function resizeProcess(width, height) {
        var p = document.createElement("p");
        p.innerText = "時間:" + new Date().toLocaleTimeString() + ", 寬:" + width + ", 高:" + height;
        document.body.appendChild(p);
    }
    //Ext封裝的resize事件
    Ext.EventManager.onWindowResize(function (width, height) {
        resizeProcess(width, height);
    });
});

 

如圖,每次改變視窗大小,resizeProcess只執行了一次。

Ext.EventManager還有on/addListener、un/removeListener等函式,這些函式都是都過Ext.lib.Event實現的,這裡就不再累述了。

Ext.EventObject

Ext.EventObject是對事件的封裝,它提供了豐富的工具函式,幫助我們獲得事件相關的資訊。通過Ext.EventObject幫助文件可以瞭解到,它包含的許多函式都與Ext.lib.Event中的函式功能是相同甚至同名的,如getPageX()、getPageY()、getPageXY()和getTarget()等,這些函式實際上都是通過Ext.lib.Event實現的。

Ext.EventObject對Ext.lib.Event擴充套件的部分是對滑鼠事件和按鍵事件的增強,定義了一系列按鍵,可以用來判斷某個鍵是否被按下:

Ext.get("text").on("keypress", function (e) {
    if (e.getKey() == Ext.EventObject.SPACE) {
        Ext.Msg.alert("提示", "你按了空格鍵!");
    }
});

Ext.EventObject將瀏覽器事件和自定義事件結合在一起使用,是對事件的封裝。如果要獲得瀏覽器原始的事件,可通過Ext.EventObject的browserEvent獲得。但這種原生事件在不同瀏覽器中可能會有很大差異,所以Ext.EventObject雖然提供該功能,但一般不建議使用。

給Ext元件新增事件處理函式

新增原生瀏覽器事件處理函式

我們已經知道可以通過 on/addListener的方式給HTML元素新增事件處理函式,Ext元件也可以通過這種方式新增,如下程式碼所示:
var text = new Ext.form.TextField({
    id: "text", renderTo: Ext.getBody()
});
Ext.get("text").on("mouseover", function (e) {
    alert("mouse over.");
});
//也可以一次新增多個事件處理函式:
Ext.get("text").on({
    "mouseover": function (e) {
        alert("mouse over.");
    },
    "mouseout": function (e) {
        alert("mouse out.");
    }
});

這種方式可以給任何原生瀏覽器所支援的事件新增處理函式。但這種方式不能用於容器類的Ext元件,如Ext.form.FieldSet、Ext.form.FormPanel和Ext.Toolbar等。

新增Ext元件事件處理函式

幾乎所有Ext元件根據自身的特性對原生事件都行了擴充套件,另外封裝了一套屬於自己的事件,這些事件的處理函式會能接收到與該元件相關的事件引數資訊。下面程式碼是給Ext元件新增事件的兩種方式:

var text1 = new Ext.form.TextField({
    id: "text1", renderTo: Ext.getBody()
});
//任何一個關於導航類鍵(arrows、tab、enter、esc等)被敲擊則觸發此事件
Ext.getCmp("tex1t").on("specialkey", function (field,e) {
    alert(field.getValue() + "," + e.getKey());
});

//也可以在元件建立的時候新增事件處理函式:
var text2 = new Ext.form.TextField({
    id: "text2", renderTo: Ext.getBody(),
    listeners: {
        change: function (field, newValue, oldValue) {
            alert("change:" + newValue);
        },
        blur: function (field) {
            alert("blur:" + field.getValue());
        }
    }
});

但這種方式並不支援所有的原生瀏覽器事件,比如給 Ext.form.TextField 元件通過上面的方式新增 mosuseover 事件處理函式是沒有效果的。

還有一種通過 handler 屬性給 Ext 按鈕元件新增事件的方式,這種方式只針對Ext按鈕元件,如下:

var button = new Ext.Button({
    id: 'button',
    text:'按鈕',
    renderTo: Ext.getBody(),
    handler: function () {
        alert("Clicked!!!");
    }
});

移除事件處理函式

我們已經知道可通過un/removeListener移除某個事件處理函式。值得注意的事,對於原生瀏覽器事件,用Ext.fly獲得元素的方式新增的事件處理函式必須用Ext.fly獲得元素的方式移除,同理,Ext.get也是一樣。但一般我們用Ext.fly而不用Ext.get獲得元素的方式新增事件處理函式,原因Ext.fly更省記憶體。對於Ext元件事件,則必須通過Ext.getCmp獲得元件的方式移除事件處理函式。如下程式碼所示:

var text = new Ext.form.TextField({
    id: "text", renderTo: Ext.getBody(),
    listeners: {
        change: function (field, n, o) {
            alert("new value : " + n);
        }
    }
});
//事件處理函式
var handlerFn = function (e) {
    alert("mouse over.");
};

//新增mouseover事件處理函式。
Ext.get("text").on("mouseover", handlerFn);
//移除mouseover事件指定引用的處理函式。
Ext.get("text").removeListener("mouseover", handlerFn);
//移除mouseover事件所有的處理函式。
Ext.get("text").removeListener("mouseover");
//用fly獲得元素的方式不能移除mouseover處理函式,因為該處理函式是通過get獲取元素新增的。
Ext.fly("text").removeListener("mouseover");
//同樣,用getCmp獲得元件的方式也不能移除mouseover處理函式。
Ext.getCmp("text").removeListener("mouseover");
//移除text元素所有原生瀏覽器事件的所有處理函式。
Ext.get("text").removeAllListeners();
//獲得元件的方式移除change事件所有的處理函式。
Ext.getCmp("text").removeListener("change");

對事件的一些額外的控制

事件的額外控制包括讓事件只被觸發一次、延遲事件處理和控制多次觸發事件的間隔等。通過on/addListener函式的第4個引數的屬性來實現,讓我們通過下面程式碼來看看常見的幾個:

var button = new Ext.Button({
    id: 'button',
    text: '按鈕',
    renderTo: Ext.getBody()
});
button.on("click",
    function () {
        var el = document.createElement("p");
        el.innerHTML = new Date().toLocaleTimeString();
        document.body.appendChild(el);
    }, this, {
        single: true,//只會執行一次單擊事件。
        buffer: 1000, //間隔1秒響應,在響應前點選無效。
        delay: 1000,//從事件觸發開始,1後才會執行處理函式。
        stopPropagattion: true,//事件不會向上傳遞(即停止事件冒泡)。
        preventDefault: true //停止事件預設操作。
        //...
    }
);

 

結束語

ExtJS的事件模型比較複雜,提供的事件處理函式也非常之多,本文短短篇幅不可能面面具到,只是把常用的做了簡單介紹。本人用ExtJS也不久,不免有錯差。
希望園友們不吝指教,多多交流,隨手點個推薦,以助大家在ExtJS學習之路上快束進步。

 

參考:
《深入淺出 Ext JS》
Ext JS API

相關文章