javascript快速入門17--事件

水之原發表於2013-12-01

事件(上)

JavaScript事件列表
事件 解說
一般事件 onclick 滑鼠點選時觸發此事件
ondblclick 滑鼠雙擊時觸發此事件
onmousedown 按下滑鼠時觸發此事件
onmouseup 滑鼠按下後鬆開滑鼠時觸發此事件
onmouseover 當滑鼠移動到某物件範圍的上方時觸發此事件
onmousemove 滑鼠移動時觸發此事件
onmouseout 當滑鼠離開某物件範圍時觸發此事件
onkeypress 當鍵盤上的某個鍵被按下並且釋放時觸發此事件.
onkeydown 當鍵盤上某個按鍵被按下時觸發此事件
onkeyup 當鍵盤上某個按鍵被按放開時觸發此事件
頁面相關事件 onabort 圖片在下載時被使用者中斷
onbeforeunload 當前頁面的內容將要被改變時觸發此事件
onerror 出現錯誤時觸發此事件
onload 頁面內容完成時觸發此事件
onmove 瀏覽器的視窗被移動時觸發此事件
onresize 當瀏覽器的視窗大小被改變時觸發此事件
onscroll 瀏覽器的滾動條位置發生變化時觸發此事件
onstop 瀏覽器的停止按鈕被按下時觸發此事件或者正在下載的檔案被中斷
oncontextmenu 當彈出右鍵上下文選單時發生
onunload 當前頁面將被改變時觸發此事件
表單相關事件 onblur 當前元素失去焦點時觸發此事件
onchange 當前元素失去焦點並且元素的內容發生改變而觸發此事件
onfocus 當某個元素獲得焦點時觸發此事件
onreset 當表單中RESET的屬性被激發時觸發此事件
onsubmit 一個表單被遞交時觸發此事件

瞭解上面的事件如此簡單,那麼事件還有什麼可講的呢?

問題一:每個事件只能註冊一個函式

    var oDiv = document.getElementById("oDiv");
    oDiv.onclick = fn1;
    oDiv.onclick =fn2;
    function fn1() {alert("我被覆蓋了!")}
    function fn2() {alert("只有我被執行到!")}

 

解決方案一:

    obj.onclick = function () {
        fn1();
        fn2();
        fn3();
    };

 

缺陷一:需要將所有函式一次新增進去,不能在執行時新增

缺陷二:在事件處理函式中this將指向window,而不是obj

解決方案二:

    function addEvent(fn,evtype,obj) {
        //obj是要新增事件的HTML元素物件
        //evtype是事件名字,不包含on字首,因為每個都有on,所以寫個on是多餘的
        //fn是事件處理函式
        var oldFn;
        if (obj["on"+evtype] instanceof Function) {
            oldFn = obj["on"+evtype];//當新增函式時,如果已註冊過了,則將其儲存起來
        }
        obj["on"+evtype]=function () {
                if (oldFn) {
                    oldFn.call(this);
                }
                fn.call(this);//使用call方法,使事件處理函式中的this仍指向obj
        };
    }

 

這樣已經解決了問題,但如何刪除事件呢?如果直接將物件的onevtype這類的屬性賦值為null將會刪除所有的事件處理函式!

解決方案二的修改版:先將事件儲存起來,儲存在物件的__EventHandles屬性裡面

    eventHandlesCounter=1;//計數器,將統計所有新增進去的函式的個數,0位預留作其它用
    function addEvent(fn,evtype,obj) {
        if (!fn.__EventID) {//__EventID是給函式加的一個標識,見下面給函式新增標識的部分
            fn.__EventID=eventHandlesCounter++;
            //使用一個自動增長的計數器作為函式的標識以保證不會重複
        }
        if (!obj.__EventHandles) {
            obj.__EventHandles=[];//當不存在,也就是第一次執行時,建立一個,並且是陣列
        }
        if (!obj.__EventHandles[evtype]) {//將所有事件處理函式按事件型別分類存放
            obj.__EventHandles[evtype]=[];//當不存在時也建立一個陣列
            if (obj["on"+evtype] instanceof Function) {
                //檢視是否已經註冊過其它函式
                //如果已經註冊過,則將以前的事件處理函式新增到陣列下標為0的預留的位置
                obj.__EventHandles[evtype][0]=obj["on"+evtype];
                obj["on"+evtype]=handleEvents;//使用handleEvents集中處理所有的函式
            }
        }
        obj.__EventHandles[evtype][fn.__EventID]=fn;
        //如果函式是第一次註冊為事件處理函式,那麼它將被新增到陣列中,函式的標識作為下標
        //如果函式已經註冊過相同物件的相同事件了,那麼將覆蓋原來的而不會被新增兩次
        function handleEvents() {
            var fns = obj.__EventHandles[evtype];
            for (var i=0;i< fns.length;i++) {
                fns[i].call(this);
            }
        }
    }

 

使用上面的函式已經可以在一個物件新增多個事件處理函式,在函式內部this關鍵字也指向了相應的物件,並且這些函式都被作了標識,那麼移除某個事件處理函式就是輕而易舉的了!

    //使用傳統方法:obj.onevtype = null;但這樣會移除所有的事件處理函式
    function delEvent(fn,evtype,obj) {
        if (!obj.__EventHandles || !obj.__EventHandles[evtype] || !fn.__EventID) {
            return false;
        }
        if (obj.__EventHandles[evtype][fn.__EventID] == fn) {
            delete obj.__EventHandles[evtype][fn.__EventID];
        }
    }

 

事件(下)

事件物件——Event

事件物件是用來記錄一些事件發生時的相關資訊的物件。事件物件只有事件發生時才會產生,並且只能是事件處理函式內部訪問,在所有事件處理函式執行結束後,事件物件就被銷燬!

訪問事件物件:W3C DOM方法與IE專用方法

    //W3C DOM把事件物件作為事件處理函式的第一個引數傳入進去
    document.onclick = function (evt) {//這樣,事件物件只能在對應的事件處理函式內部可以訪問到
        alert(evt);
    };
    //IE將事件物件作為window物件的一個屬性(相當於全域性變數)
    //貌似全域性物件,但是隻有是事件發生時才能夠訪問
    alert(window.event);//null
    window.onload = function () {
        alert(window.event);
    };

 

事件物件的屬性及方法

滑鼠相關
屬性名值型別讀/寫描述
button Integer R

對於特定的滑鼠事件,表示按下的滑鼠按鈕,該屬性僅可以在mouseup與mousedown事件中訪問。W3C 規定:0表示按下了左鍵,1表示按下了中鍵,2表示按下了右鍵,相當於對於滑鼠鍵從左到右進行的編號,而編號從0開始; 而IE有另外一套規定:0表示沒有任何鍵按下,1表示左鍵,2表示右鍵,4表示中鍵,而其它按鍵的組合則只要將鍵碼相加即可,如:同時按下左右鍵時button值為3

clientX Integer R 事件發生時,滑鼠在客戶端區域的X座標,客戶端區域是指頁面可視區域
clientY Integer R 事件發生時,滑鼠在客戶端區域的Y座標
screenX Integer R(IE)
R/W(W3C)
相對於螢幕的滑鼠X座標
screenY Integer R(IE)
R/W(W3C)
相對於螢幕的滑鼠Y座標
x(僅IE) Integer R 滑鼠相對於引起事件的元素的父元素的X座標
y(僅IE) Integer R 滑鼠相對於引起事件的元素的父元素的Y座標
offsetX(僅IE)
layerX(僅W3C)
Integer R 滑鼠相對於引起事件的物件的X座標
offsetY(僅IE)
layerY(僅W3C)
Integer R 滑鼠相對於引起事件的物件的Y座標
pageX(僅W3C) Integer R 滑鼠相對於頁面的X座標
pageY(僅W3C) Integer R 滑鼠相對於頁面的Y座標

 

鍵盤相關
屬性名值型別讀/寫描述
altKey Boolean R true表示按下了ALT鍵;false表示沒有按下
ctrlKey Boolean R true表示按下了CTROL,false表示沒有
shiftKey Boolean R true表示按下了shift,false表示沒有
keyCode Integer R/W(IE)
R(W3C)
對於keypress事件,表示按下按鈕的Unicode字元;對於keydown/keyup事件 ,表示按下按鈕的數字代號
charCode(僅W3C) Integer R 在keypress事件中所按鍵的字元Unicode編碼,如果不是字元鍵,則該屬性為0,並且,當CapsLock開啟與關閉時charCode的值也對應著大小寫字母

 其它

屬性名值型別讀/寫描述
srcElement(IE)
target(W3C)
Element R 引起事件的元素
fromElement(僅IE) Element R 某些滑鼠事件中(mouseover與mouseout),滑鼠所離開的元素
toElement(僅IE) Element R 某些滑鼠事件中(mouseover與mouseout),滑鼠所進入的元素
relatedTarget(僅W3C) Element R 某些滑鼠事件中(mouseover與mouseout),返回與事件的目標節點相關的節點。
repeat(僅IE) Boolean R 如果不斷觸發keydown事件,則為true,否則為false
returnValue(僅IE) Boolean R/W 將其設為false表示以取消事件的預設動作
preventDefault(僅W3C) Function R 執行方法以取消事件的預設動作
type String R 事件的名稱,不帶on字首
cancelable(僅W3C ) Boolean R 當為true表示事件的預設動作可以被取消(用preventDefault方法取消)
cancelBubble(僅IE) Boolean R/W 將其設定為true將取消事件冒泡
stopPropagation(僅W3C) Function R 執行方法取消事件冒泡
bubbles(僅W3C) Boolean R 返回true表示事件是冒泡型別
eventPhase(僅W3C) Integer R 返回事件傳播的當前階段。它的值是下面的三個常量之一,它們分別表示捕獲階段、在目標物件上時和起泡階段:
常量
Event.CAPTURING_PHASE(捕獲階段) 1
Event.AT_TARGET(在目標物件上) 2
Event.BUBBLING_PHASE(冒泡階段) 3
timeStamp (僅W3C) Long R 返回一個時間戳。指示發生事件的日期和時間(從 epoch 開始的毫秒數)。epoch 是一個事件參考點。在這裡,它是客戶機啟動的時間。並非所有系統都提供該資訊,因此,timeStamp 屬性並非對所有系統/事件都是可用的。

取得事件物件及取得事件目標物件

    document.onclick =function (evt) {
        evt = evt || window.event;//在IE中evt會是undefined
        //而支援W3C DOM事件的瀏覽器中事件物件將會作為事件處理函式的第一個引數
        var targetElement = evt.target || evt.srcElement;
        //IE中事件物件沒有target屬性
    };

 

阻止事件發生時瀏覽器的預設行為

    document.onclick = function (evt) {
        evt = evt || window.event;
        var target = evt.target || evt.srcElement;
        if (!target) {
            return;
        }
        if (target.tagName=="A" && target.href) {
            //使用傳統的方法取消事件預設行為必須使用return false
            //但使用了return ,函式便終止了執行,可以使用事件物件來取消
            if (window.event) {//IE
                window.event.returnValue = false;
            } else {
                evt.preventDefault();
            }
            window.open(target.href,"newWindow");
            //這樣讓所有的連結在新視窗開啟
        }
    };
    

事件傳播——冒泡與捕獲

DOM事件標準定義了兩種事件流,這兩種事件流有著顯著的不同並且可能對你的應用有著相當大的影響。這兩種事件流分別是捕獲和冒泡。和許多Web技術一樣,在它們成為標準之前,Netscape和微軟各自不同地實現了它們。Netscape選擇實現了捕獲事件流,微軟則實現了冒泡事件流。幸運的是,W3C決定組合使用這兩種方法,並且大多數新瀏覽器都遵循這兩種事件流方式。

預設情況下,事件使用冒泡事件流,不使用捕獲事件流。然而,在Firefox和Safari裡,你可以顯式的指定使用捕獲事件流,方法是在註冊事件時傳入useCapture引數,將這個引數設為true。

冒泡事件流

當事件在某一DOM元素被觸發時,例如使用者在客戶名位元組點上點選滑鼠,事件將跟隨著該節點繼承自的各個父節點冒泡穿過整個的DOM節點層次,直到它遇到依附有該事件型別處理器的節點,此時,該事件是onclick事件。在冒泡過程中的任何時候都可以終止事件的冒泡,在遵從W3C標準的瀏覽器裡可以通過呼叫事件物件上的stopPropagation()方法,在Internet Explorer裡可以通過設定事件物件的cancelBubble屬性為true。如果不停止事件的傳播,事件將一直通過DOM冒泡直至到達文件根。

捕獲事件流

事件的處理將從DOM層次的根開始,而不是從觸發事件的目標元素開始,事件被從目標元素的所有祖先元素依次往下傳遞。在這個過程中,事件會被從文件根到事件目標元素之間各個繼承派生的元素所捕獲,如果事件監聽器在被註冊時設定了useCapture屬性為true,那麼它們可以被分派給這期間的任何元素以對事件做出處理;否則,事件會被接著傳遞給派生元素路徑上的下一元素,直至目標元素。事件到達目標元素後,它會接著通過DOM節點再進行冒泡。

現代事件繫結方法

針對如上節課所討論的,使用傳統事件繫結有許多缺陷,比如不能在一個物件的相同事件上註冊多個事件處理函式。而瀏覽器和W3C也並非沒有考慮到這一點,因此在現代瀏覽器中,它們有自己的方法繫結事件。

W3C DOM

  • obj.addEventListener(evtype,fn,useCapture)——W3C提供的新增事件處理函式的方法。obj是要新增事件的物件,evtype是事件型別,不帶on字首,fn是事件處理函式,如果useCapture是true,則事件處理函式在捕獲階段被執行,否則在冒泡階段執行
  • obj.removeEventListener(evtype,fn,useCapture)——W3C提供的刪除事件處理函式的方法

微軟IE方法

  • obj.attachEvent(evtype,fn)——IE提供的新增事件處理函式的方法。obj是要新增事件的物件,evtype是事件型別,帶on字首,fn是事件處理函式,IE不支援事件捕獲
  • obj.detachEvent(evtype,fn,)——IE提供的刪除事件處理函式的方法,evtype包含on字首

整合兩者的方法

    function addEvent(obj,evtype,fn,useCapture) {
        if (obj.addEventListener) {
            obj.addEventListener(evtype,fn,useCapture);
        } else {
            obj.attachEvent("on"+evtype,fn);//IE不支援事件捕獲
        } else {
            obj["on"+evtype]=fn;//事實上這種情況不會存在
        }
    }
    function delEvent(obj,evtype,fn,useCapture) {
        if (obj.removeEventListener) {
            obj.removeEventListener(evtype,fn,useCapture);
        } else {
            obj.detachEvent("on"+evtype,fn);
        } else {
            obj["on"+evtype]=null;
        }
    }

 

其它相容性問題:IE不支援事件捕獲?很抱歉,這個沒有辦法解決!但IE的attach方法有個問題,就是使用attachEvent時在事件處理函式內部,this指向了window,而不是obj!當然,這個是有解決方案的!

    function addEvent(obj,evtype,fn,useCapture) {
        if (obj.addEventListener) {
            obj.addEventListener(evtype,fn,useCapture);
        } else {
            obj.attachEvent("on"+evtype,function () {
                fn.call(obj);
            });
        } else {
            obj["on"+evtype]=fn;//事實上這種情況不會存在
        }
    }

 

但IE的attachEvent方法有另外一個問題,同一個函式可以被註冊到同一個物件同一個事件上多次,解決方法:拋棄IE的attachEvent方法吧!IE下的attachEvent方法不支援捕獲,和傳統事件註冊沒多大區別(除了能繫結多個事件處理函式),並且IE的attachEvent方法存在記憶體洩漏問題!

addEvent,delEvent現代版

    function addEvent(obj,evtype,fn,useCapture) {
        if (obj.addEventListener) {//優先考慮W3C事件註冊方案
            obj.addEventListener(evtype,fn,!!useCapture);
        } else {//當不支援addEventListener時(IE),由於IE同時也不支援捕獲,所以不如使用傳統事件繫結
            if (!fn.__EventID) {fn.__EventID = addEvent.__EventHandlesCounter++;}
            //為每個事件處理函式分配一個唯一的ID
            
            if (!obj.__EventHandles) {obj.__EventHandles={};}
            //__EventHandles屬性用來儲存所有事件處理函式的引用
            
            //按事件型別分類
            if (!obj.__EventHandles[evtype]) {//第一次註冊某事件時
                obj.__EventHandles[evtype]=[];
                if (obj["on"+evtype]) {//以前曾用傳統方式註冊過事件處理函式
                    (obj.__EventHandles[evtype][0]=obj["on"+evtype]).__EventID=0;//新增到預留的0位
                    //並且給原來的事件處理函式增加一個ID
                }
                obj["on"+evtype]=addEvent.execEventHandles;
                //當事件發生時,execEventHandles遍歷陣列obj.__EventHandles[evtype]並執行其中的函式
            }
        }
    }
    addEvent.__EventHandlesCounter=1;//計數器,0位預留它用
    addEvent.execEventHandles = function (evt) {//遍歷所有的事件處理函式並執行
        if (!this.__EventHandles) {return true;}
        evt = evt || window.event;
        var fns = this.__EventHandles[evt.type];
        for (var i=0;i< fns.length;i++) {
            if (fns[i] instanceof Function) {
                fns[i].call(this);
            }
        }
    };
    function delEvent(obj,evtype,fn,useCapture) {
        if (obj.removeEventListener) {//先使用W3C的方法移除事件處理函式
            obj.removeEventListener(evtype,fn,!!useCapture);
        } else {
            if (obj.__EventHandles) {
                var fns = obj.__EventHandles[evtype];
                if (fns) {delete fns[fn.__EventID];}
            }
        }
    }

 

標準化事件物件

IE的事件物件與W3C DOM的事件物件有許多不一樣的地方,解決的最好的方法就是調整IE的事件物件,以使它儘可能的與標準相似!下表列出了IE事件物件中一些和W3C DOM名稱或值不一樣但含義相同的屬性

IE與W3C DOM事件物件的不同
W3C DOMIE
button——按鍵編碼為:0-左鍵,1-中鍵,2-右鍵 button——按鍵編碼為:1-左鍵,2-右鍵,4-中鍵
charCode 沒有對應屬性,但可以用keyCode來代替
preventDefault 沒有對應方法,但可以將event物件的returnValue設為false來模擬
target srcElement
relatedTarget fromElement與toElement
stopPropagation 沒有對應方法,但可以通過將event物件的cancelBubble屬性設為true來模擬

總結出fixEvent函式

    function fixEvent(evt) {
        if (!evt.target) {
            evt.target = evt.srcElement;
            evt.preventDefault = fixEvent.preventDefault;
            evt.stopPropagation = fixEvent.stopPropagation;
            if (evt.type == "mouseover") {
                evt.relatedTarget = evt.fromElement;
            } else if (evt.type =="mouseout") {
                evt.relatedTarget = evt.toElement;
            }
            evt.charCode =  (evt.type=="keypress")?evt.keyCode:0;
            evt.eventPhase = 2;//IE僅工作在冒泡階段
            evt.timeStamp = (new Date()).getTime();//僅將其設為當前時間
        }
        return evt;
    }
    fixEvent.preventDefault =function () {
        this.returnValue = false;//這裡的this指向了某個事件物件,而不是fixEvent
    };
    fixEvent.stopPropagation =function () {
        this.cancelBubble = true;
    };

 

fixEvent函式不是單獨執行的,它必須有一個事件物件引數,而且只有事件發生時它才被執行!最好的方法是把它整合到addEvent函式的execEventHandles裡面

    addEvent.execEventHandles = function (evt) {//遍歷所有的事件處理函式並執行
        if (!this.__EventHandles) {return true;}
        evt = fixEvent(evt || window.event);//在這裡對其進行標準化操作
        var fns = this.__EventHandles[evt.type];
        for (var i=0;i< fns.length;i++) {
            if (fns[i] instanceof Function) {
                fns[i].call(this,evt);//並且將其作為事件處理函式的第一個引數
                //這樣在事件處理函式內部就可以使用統一的方法訪問事件物件了
            }
        }
    };

 

Load事件

使用JavaScript操縱DOM,必須等待DOM載入完畢才可以執行程式碼,但window.onload有個壞處,它非要等到頁面中的所有圖片及視訊載入完畢才會觸發load事件。結果就是一些本來應該在開啟時隱藏起來的元素,由於網路延遲,在頁面開啟時仍然會出現,然後又會突然消失,讓使用者覺得莫名其妙。必須與這種醜陋的閃爍告別!

    function addLoadEvent(fn) {
        var init = function() {
            if (arguments.callee.done) return;
            arguments.callee.done = true;
            fn.apply(document,arguments);
        };
        //註冊DOMContentLoaded事件,如果支援的話
        if (document.addEventListener) {
            document.addEventListener("DOMContentLoaded", init, false);
        }
        //但對於Safari,我們需要使用setInterval方法不斷檢測document.readyState
        //當為loaded或complete的時候表明DOM已經載入完畢
        if (/WebKit/i.test(navigator.userAgent)) {
            var _timer = setInterval(function() {
                if (/loaded|complete/.test(document.readyState)) {
                    clearInterval(_timer);
                    init();
                }
            },10);
        }
        //對於IE則使用條件註釋,並使用script標籤的defer屬性
         //IE中可以給script標籤新增一個defer(延遲)屬性,這樣,標籤中的指令碼只有當DOM載入完畢後才執行
         /*@cc_on @*/
         /*@if (@_win32)
         document.write("<script id=\"__ie_onload\" defer=\"defer\" src=\"javascript:void(0)\"><\/script>");
         var script = document.getElementById("__ie_onload");
         script.onreadystatechange = function() {
             if (this.readyState == "complete") {
                 init();
             }
         };
         /*@end @*/
         return true;
     }

 

相關文章