1. 程式人生 > >《JavaScript高階程式設計》筆記:事件(十三)

《JavaScript高階程式設計》筆記:事件(十三)

事件流

事件冒泡

IE的事件流叫做事件冒泡,即事件開始時由最具體的元素接收,然後逐級向上傳播到較為不具體的節點(文件)。如下程式碼:

<body>
    <div id="myDiv">click me</div>
</body>

如果你點選了上面的div元素,那麼這個click事件會按照如下順序傳播:

  • (1)<div>
  • (2)<body>
  • (3)<html>
  • (4)document

所有現代瀏覽器都支援事件冒泡,但在具體實現上還是有一些差別。IE5.5以及更早的版本事件冒泡會跳過<html>元素(從<body>直接跳到document)。IE9及其它瀏覽器則將事件一直冒泡到window物件。

事件捕獲

事件捕獲的思想是不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。如上面的例子,單機div元素,那麼就會按照以下順序觸發click事件:

  • (1)document
  • (2)<html>
  • (3)<body>
  • (4)<div>

由於老版本瀏覽器的不支援,因此很少有人使用事件捕獲。

DOM事件流

“DOM2級事件”規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。

事件處理程式

HTML事件處理程式

<input type="button" value="click me"
onclick="alert(this.value)"/>

DOM0級事件處理程式

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    console.log(this.value);
}

DOM2級事件處理程式

“DOM2級事件”定義了兩個方法,用於處理指定和刪除事件處理程式的操作:addEventListener()removeEventListener()。接受三個引數:要處理的事件名、作為事件處理程式的函式和一個布林值。布林值為true,表示在捕獲階段呼叫事件處理程式;如果為false,表示在冒泡階段呼叫事件處理程式。

var btn = document.getElementById('myBtn');
btn.addEventListener('click',function(){
    alert(this.value)
},false)

IE事件處理程式

IE中實現了與DOM中類似的兩個方法:attachEvent()detachEvent()。這兩個方法接受相同的兩個引數:事件處理程式名稱和事件處理程式函式。由於IE8及更早版本只支援事件冒泡,所以通過attachEvent()新增的事件處理程式都會被新增到冒泡階段。

btn.attachEvent('onclick',function(){
    alert(this.value)  //undefined
})

結果為undefined的原因是,在使用attachEvent()方法的情況下,事件處理程式會在全域性作用域中執行。因此this等於window。在編寫跨瀏覽器區別的時候,牢記這一區別非常重要。

attachEvent()方法也可以為一個元素新增多個事件處理程式,如下:

btn.attachEvent('onclick',function(){
    alert('clicked')
})
btn.attachEvent('onclick',function(){
    alert('hello world!')
})

在IE9以及更改版本瀏覽器得到的結果順序跟addEventListener()的順序一樣,結果是先clicked,後hello world!。在IE8以及以前版本得到順序是相反的。

跨瀏覽器的事件處理程式

var EventUtil = {
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListener(type,handler,false);
        }else if(element.attachEvent){
            element.attachEvent("on" + type,handler);
        }else{
            element["on" + type] = handler;
        }
    },
    removeHandler:function(element,type,handler){
        if(element.removeEventListener){
            element.removeEventListener(type,handler,false);
        }else if(element.detachEvent){
            element.detachEvent("on" + type,handler);
        }else{
            element["on" + type] = null;
        }
    }
}

呼叫方式:

var handler = function(){
    alert('clicked');
}
EventUtil.addHandler(btn,"click",handler);

//這裡省略其它程式碼
EventUtil.removeHandler(btn,"click",handler);

事件物件

 DOM中的事件物件

相容DOM的瀏覽器會將一個event物件傳入到事件處理程式中,無論指定事件處理程式時用什麼方法(DOM0級或DOM2級),都會傳入event物件。下面的例子:

var btn = document.getElementById('myBtn');

btn.onclick = function(event){
    console.log(event.type); //click
}
btn.addEventListener('click',function(event){
    console.log(event.type); //click
},false)

在通過HTML特性指定事件處理程式時,變數event中儲存著event物件,下面例子:

<input type="button" value="click me" onclick="alert(event.type)"/>

event物件包含與建立它的特定事件有關的屬性和方法。觸發的事件型別不一樣,可用的屬性和方法也不一樣。不過,所有事件都會有下表列出的成員:

  •  bubbles:(Boolean)只讀,表明事件是否冒泡。
  • cancelable:(Boolean)只讀,表明是否可以取消事件的預設行為。
  • currentTarget:(Element)只讀,其事件處理程式當前正在處理事件的那個元素。
  • defaultPrevented:(Boolean)只讀,為true表明已經呼叫了preventDefault()(DOM3級事件中新增)。
  • detail:(Integer)只讀,與事件相關的細節資訊。
  • eventPhase:(Integer)只讀,呼叫事件處理程式的階段:1表示捕獲階段,2表示“處於目標”,3表示冒泡階段。
  • preventDafult():(Function)只讀,取消事件的預設行為。如果cancelable是true,則可以使用這個方法。
  • stopImmediatePropagation():(Function)只讀,取消事件的進一步捕獲或冒泡,同時阻止任何事件處理程式被呼叫(DOM3級事件中新增)。
  • stopPropagation():(Function)只讀,取消事件的進一步捕獲或冒泡。如果過bubbles為true,則可以使用這個方法。
  • target:(Element)只讀,事件的目標。
  • trusted:(Boolean)只讀,為true表示事件是瀏覽器生成的,為false表示事件是有開發人員通過JavaScript建立的(DOM3級事件中新增)。
  • type:(String)只讀,被觸發的事件的型別。
  • view:(AbstractView)只讀,與事件關聯的抽象檢視。等同於發生事件的window物件。

在事件處理程式內部,物件this始終等於currentTarget的值,而target只包含事件的實際目標。如果直接將事件處理程式指向給了目標元素,則this,currentTarget和target包含相同的值,如下程式碼:

var btn = document.getElementById('myBtn');

btn.addEventListener('click',function(event){
    console.log(event.currentTarget === this); //true
    console.log(event.target === this); //true
},false)

如果事件處理程式存在於按鈕的父節點中(例如document.body),那麼這些值是不相等的,如下程式碼:

var btn = document.getElementById('myBtn');

document.body.addEventListener('click',function(event){
    console.log(event.currentTarget === document.body); //true
    console.log(document.body === this); //true
    console.log(event.target === btn); //true
},false)

 在需要一個函式處理多個事件時,可以使用type屬性,如下:

var btn = document.getElementById('myBtn');
var handler = function(event){
    switch(event.type){
        case "click":
            console.log("clicked");
            break;
        case "mouseover":
            event.target.style.backgroundColor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundColor ="";
            break;
    }
};

btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

阻止事件的預設行為,使用preventDefault()方法,比如阻止a標籤的跳轉,如下程式碼:

var link = document.getElementById('myLink');
link.onclick = function(event){
    event.preventDefault();
}

只有cancelable屬性設定為true時,才可以使用preventDefault()來取消其預設行為。

stopPropagation()方法用於立即停止事件在DOM層次中的傳播,寄取消進一步的事件捕獲或冒泡。例如直接新增到一個按鈕的事件處理程式可以呼叫stopPropagation(),從而避免觸發註冊在document.body上面的事件處理程式,如下程式碼:

var btn = document.getElementById('myBtn');
btn.onclick = function(event){
    console.log('clicked');
    event.stopPropagation();
}
document.body.onclick = function(){
    console.log('body clicked');
}

結果只打印出了clicked。

事件物件的eventPhase屬性,可以用來確定事件當前正處理事件流的哪個階段。1表示捕獲階段,2表示“處於目標”,3表示冒泡階段。

var btn = document.getElementById('myBtn');
btn.onclick = function(event){
    console.log(event.eventPhase);//2
};

document.body.addEventListener('click',function(event){
    console.log(event.eventPhase);//1
},true);

document.body.onclick = function(){
    console.log(event.eventPhase);//3
};

顯示結果順序分別為1,2,3。

 IE中的事件物件

與訪問DOM中的event物件不同,要訪問IE中的event物件有幾種不同的方式,取決於事件處理程式的方法。在使用DOM0級方法新增事件處理程式時,event物件作為window物件的一個屬性存在,如下例子:

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    var event = window.event;
    alert(event.type); //click
};

如果事件程式是使用attachEvent()新增的,那麼就會有一個event物件作為引數被傳遞到事件處理程式函式中,如下程式碼:

var btn = document.getElementById('myBtn');
btn.attachEvent('onclick',function(event){
    alert(event.type); //click
})

 IE中的event物件包含下面的屬性和方法:

  • cancelBubble:(Boolean)讀/寫,預設值為false,將其設定為true就可以取消事件冒泡(與DOM中的stopPropagation()方法的作用相同)。
  • returnValue:(Boolean)讀/寫,預設值為true,將其設定為false可以取消事件的預設行為(與DOM中的preventDafult()方法的作用相同)。
  • srcElement:(Element)只讀,事件的目標(與DOM中的target屬性相同)。
  • type:(String)只讀,被觸發的事件的型別。

 因為事件處理程式中的作用域是根據指定它的方式來確定的,所以不能認為this會始終等於事件目標。故而,最好還是使用event.srcElement比較保險,如下程式碼:

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    alert(window.event.srcElement === this); //true
};

btn.attachEvent('onclick',function(event){
    alert(this); //window
    alert(window.event.srcElement === this); //false
})

取消預設行為:

var link = document.getElementById('myLink');
link.onclick = function(){
    window.event.returnValue = false;
};

停止事件冒泡:

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    alert('clicked');
    window.event.cancelBubble = true;
};

document.body.attachEvent('onclick',function(event){
    alert('body clicked')
})

 跨瀏覽器的事件物件

var EventUtil = {
    
    //事件處理程式
    addHandler:function(element,type,handler){
        if(element.addEventListener){
            element.addEventListener(type,handler,false);
        }else if(element.attachEvent){
            element.attachEvent("on" + type,handler);
        }else{
            element["on" + type] = handler;
        }
    },
    
    //得到event物件
    getEvent:function(event){
        return event ? event : window.event;
    },
    
    //得到事件目標
    getTarget:function(event){
        return event.target || event.srcElement;
    },
    
    //取消預設行為
    preventDefault:function(event){
        if(event.preventDefault){
            event.preventDefault();
        }else{
            event.returnValue = false;
        }
    },
    
    //移除事件處理程式
    removeHandler:function(element,type,handler){
        if(element.removeEventListener){
            element.removeEventListener(type,handler,false);
        }else if(element.detachEvent){
            element.detachEvent("on" + type,handler);
        }else{
            element["on" + type] = null;
        }
    },
    
    //阻止事件捕獲或冒泡
    stopPropagation:function(event){
        if(event.stopPropagation){
            event.stopPropagation();
        }else{
            event.cancelBubble = true;
        }
    }
}

 事件型別

“DOM3級事件”規定如下幾類事件。

  • UI(User Interface,使用者介面)事件,當用戶與頁面上的元素互動時觸發;
  • 焦點事件,當元素獲得或者失去焦點時觸發;
  • 滑鼠事件,當用戶通過滑鼠在頁面上執行操作時觸發;
  • 滾輪事件,當滑鼠使用滾輪(或類似裝置)時觸發;
  • 文字事件,當在文件中輸入文字時觸發;
  • 鍵盤事件,當用戶通過鍵盤在頁面上執行操作時觸發;
  • 合成事件,當為IME(Input Method Editor,輸入法編輯器)輸入字元時觸發;
  • 變動(mutation)事件,當底層DOM結構發生變化時觸發;
  • 變動名稱事件,當元素或屬性名變動時觸發。此類事件已經廢除。

UI事件

  • DOMActivate:表示元素已經被使用者操作(通過滑鼠或鍵盤)啟用。這個事件在DOM3級事件中被廢除,但firefox2+和Chrome支援它。
  • load:當頁面完全載入後在window上面觸發,當所有框架都載入完畢時在框架集上面觸發,當影象載入完畢時在<img>元素上面觸發,或者當嵌入的內容載入完畢時在<object>元素上面觸發。
  • unload:當頁面完全解除安裝後在window上面觸發,當所有框架都解除安裝後在框架集上面觸發,或者當嵌入的內容解除安裝完畢後早<object>元素上面觸發。
  • abort:在使用者停止下載過程時,如果嵌入的內容沒有載入完,則在<object>元素上面觸發。
  • error:當傳送JavaScript錯誤時在window上面觸發,當無法載入圖片是在<img>元素上面觸發,當無法載入嵌入內容時在<object>元素上面觸發,或者當有一或多個框架無法載入時在框架集上面觸發。
  • select:當用戶選擇文字框(<input>或<textarea>)中的一或多個字元時觸發。
  • resize:當視窗或框架的大小發生變化時在window或框架上面觸發。
  • scroll:當用戶滾動帶滾動條的元素中的內容時,在該元素上觸發。<body>元素中包含所載入頁面的滾動條。

多數這些事件都與window物件或者表單控制元件相關。

除了DOMActivate之外,其它事件在DOM2級事件中都歸為HTML事件(DOMActivate在DOM2級事件中仍屬於UI事件)。要確定瀏覽器是否支援DOM2級事件規定的HTML事件,可以用如下程式碼:

var isSupported = document.implementation.hasFeature("HTMLEvents","2.0");

確定瀏覽器是否支援DOM3級事件定義的事件,程式碼如下:

var isSupported = document.implementation.hasFeature("UIEvent","3.0");

1.load事件

EventUtil.addHandler(window,'load',function(event){
    console.log('loaded!');
})

為<body>元素新增一個onload特性,程式碼如下:

<body onload="alert('loaded!')">
</body>

一般在window上面發生的任何事件都可以在<body/>元素中通過相應的特性來指定,因為在HTML中無法訪問到window元素。建議儘可能使用JavaScript方式。

圖片載入:

var image = document.getElementById('myImage');
EventUtil.addHandler(image,'load',function(event){
    event = EventUtil.getEvent(event);
    console.log(EventUtil.getTarget(event).src);
})

待建立新的<img>元素時,可以為其指定一個事件處理程式。此時,最重要的是在指定src屬性之前先指定事件,如下程式碼:

EventUtil.addHandler(window,'load',function(){
    var image = document.createElement('img');
    EventUtil.addHandler(image,'load',function(event){
        event = EventUtil.getEvent(event);
        console.log(EventUtil.getTarget(event).src);
    })
    document.body.appendChild(image);
    image.src = 'images/b.jpg';
})

需要格外注意的一點是:新影象元素不一定要從新增到文件後才開始下載,只要設定了src屬性就開始下載

同樣的功能可以使用DOM0級的Image物件實現,如下程式碼:

EventUtil.addHandler(window,'load',function(){
    var image = new Image();
    EventUtil.addHandler(image,'load',function(event){
        event = EventUtil.getEvent(event);
        console.log(EventUtil.getTarget(event).src);
    })
    image.src = 'images/b.jpg';
})

還有一些元素以非標準的的方式支援load事件。在IE9以及更高版本,<script>元素也會觸發load事件。

EventUtil.addHandler(window,'load',function(){
    var script = document.createElement('script');
    EventUtil.addHandler(script,'load',function(event){
        console.log('loaded!');
    })
    script.src = 'js/common.js';
    document.body.appendChild(script);
})

注:IE8以及更早版本不支援<script>元素上的load事件。

<link>元素的load事件:

EventUtil.addHandler(window,'load',function(){
    var link = document.createElement('link');
    link.type ="text/css";
    link.rel ="stylesheet";
    EventUtil.addHandler(link,'load',function(event){
        console.log('loaded!');
    })
    link.href = 'css/rest.css';
    document.getElementsByTagName('head')[0].appendChild(link)
})

與<script>節點類似,在未指定href屬性並將<link>元素新增到文件之前也不會開始下載樣式表。

2.unload事件

這個事件在文件完全被解除安裝後觸發。只要使用者從一個頁面切換到另一個頁面,就會發生unload事件。而利用這個事件最多的情況就是清除引用,以避免記憶體洩露。

EventUtil.addHandler(window,'unload',function(){
    alert('unloaded!');
})

3.resize事件

EventUtil.addHandler(window,'resize',function(){
    alert('resized!');
})

4.scroll事件

EventUtil.addHandler(window,'scroll',function(){
    if(document.compatMode == 'CSS1Compat'){
        console.log(document.documentElement.scrollTop);
    }else{
        console.log(document.body.scrollTop);
    }
})

焦點事件

焦點事件會在頁面獲得或者失去焦點時觸發。利用這些事件並與document.hasFocus()方法以及document.activeElement屬性配合,可以知曉使用者在頁面中的行蹤,以下6個焦點事件。

  • blur:在元素失去焦點時觸發。這個事件不會冒泡;所有瀏覽器都支援它。
  • DOMFocusIn:在元素獲得焦點時觸發。這個事件與HTML事件focus等價,但它冒泡。只有opera支援這個事件。DOM3級事件廢除了DOMFocusIn,選擇了focusin。
  • DOMFocusOut:在元素失去焦點時觸發。這個事件是HTML事件blur的通用版本。只有opera支援這個事件。DOM3級事件廢除了DOMFocusOut,選擇了focusout。
  • focus:在元素獲得焦點時觸發。這個事件不會冒泡;所有瀏覽器都支援它。
  • focusin:在元素獲得焦點時觸發。這個事件與HTML事件focus等價,但它冒泡。支援這個事件的瀏覽器有IE5.5+、Safari5.1+、Opera11.5+和Chrome。
  • focusout:在元素失去焦點時觸發。這個事件是HTML事件blur的通用版本。支援這個事件的瀏覽器有IE5.5+、Safari5.1+、Opera11.5+和Chrome。

IE的focusin和focusout最後被DOM3級事件採納為標準方式。

當焦點從頁面中的一個元素移動到另一個元素,會依次觸發下列事件:

(1)focusout在失去焦點的元素上觸發。

(2)focusin在獲得焦點的元素上觸發。

(3)blur在失去焦點的元素上觸發。

(4)DOMFocusOut在失去焦點的元素上觸發。

(5)focus在獲得焦點的元素上觸發。

(6)DOMFocusIn在獲得焦點的元素上觸發。

確定瀏覽器是否支援這些事件,可以使用如下程式碼:

var isSupported = document.implementation.hasFeature('FocusEvent','3.0');

滑鼠與滾輪事件

DOM3級事件中定義了9個滑鼠事件,如下:

  • click:在使用者單機主滑鼠按鈕(一般是左邊的按鈕)或者按下回車鍵時觸發。
  • dblclick:在使用者雙擊主滑鼠按鈕(一般是左邊的按鈕)時觸發。DOM3級納入了標準。
  • mousedown:在使用者按下任意滑鼠按鈕時觸發。不能通過鍵盤觸發這個事件。
  • mouseenter:在滑鼠游標從元素外部首次移動到元素範圍之內時觸發。這個事件不冒泡,而且在游標移動到後代元素上不會觸發。DOM3級事件將它納入了規範。
  • mouseleave:在位於元素上方的滑鼠游標移動到元素範圍之外時觸發。這個事件不冒泡,而且在游標移動到後代元素上不會觸發。DOM3級事件將它納入了規範。
  • mousemove:當滑鼠指標在元素內部移動時重複地觸發。不能通過鍵盤觸發這個事件。
  • mouseout:在滑鼠指標位於一個元素上方,然後使用者將其移入另一個元素時觸發。又移入的另一個元素可能位於前一個元素的外部,也可能是這個元素的子元素。不能通過鍵盤觸發這個事件。
  • mouseover:當滑鼠指標位於一個元素外部,然後使用者將其首次移入另一個元素邊界之內時觸發。不能通過鍵盤觸發這個事件。
  • mouseup:在使用者釋放滑鼠按鈕時觸發。不能通過鍵盤觸發這個事件。

頁面上的所有元素都支援滑鼠事件。除了mouseentermouseleave,所有滑鼠事件都會冒泡,也可以被取消,而取消滑鼠事件將會影響瀏覽器的預設行為。取消滑鼠事件的預設行為還會影響其他事件,因為滑鼠事件和其他事件是密不可分的關係。

只有在同一個元素上相繼觸發mousedown和mouseup事件,才會觸發click事件;如果mousedown或mouseup中的一個被取消,就不會觸發click事件。類似地,只有觸發兩次click事件,才會觸發一次dblclick事件,如果有程式碼阻止了連續兩次觸發click事件(可能是直接取消click事件,也可能通過取消mousedown或mouseup間接實現),那麼就不會觸發dblclick事件。這4個事件觸發的順序如下:

(1)mousedown

(2)mouseup

(3)click

(4)mousedown

(5)mouseup

(6)click

(7)dblclick

IE8及之前版本的實現有一個小bug,因此在雙擊事件中,會跳過第二個mousedown和click事件,其順序如下:

(1)mousedown

(2)mouseup

(3)click

(4)mouseup

(5)dblclick

IE9修復了這個bug。

使用如下程式碼可以檢測瀏覽器是否支援如上DOM2級事件(除dblckick、mouseenter和mouseleave之外):

var isSupported = document.implementation.hasFeature('MouseEvents','2.0');

檢測瀏覽器是否支援上面的所有事件,程式碼如下:

var isSupported = document.implementation.hasFeature('MouseEvent','3.0');

1.客戶區座標位置

clientXclientY他們的值表示事件發生時滑鼠指標在視口中的水平和垂直座標。

var btn = document.getElementById('myBtn');
EventUtil.addHandler(btn,'click',function(event){
    event = EventUtil.getEvent(event);
    console.log("client coordinates:" + event.clientX + "," + event.clientY);
})

2.頁面座標位置

pageXpageY,這兩個屬性表示滑鼠游標在頁面中的位置,因此座標是從頁面本身而非視口的左邊和頂邊計算的。

以下程式碼可以取得滑鼠事件在頁面中的座標:

var btn = document.getElementById('myBtn');
EventUtil.addHandler(btn,'click',function(event){
    event = EventUtil.getEvent(event);
    console.log("Page coordinates:" + event.pageX + "," + event.pageY);
})

在頁面沒有滾動的情況下,pageX和pageY的值與clientX和clientY的值相等。

IE8及更早的版本不支援事件物件上的頁面座標,不過可以使用客戶區座標和滾動資訊可以計算出來。這時候需要用到document.body(混雜模式)或document.documentElement(標準模式)中的scrollLeft和scrollTop屬性。計算程式碼如下:

var btn = document.getElementById('myBtn');
EventUtil.addHandler(btn,'click',function(event){
    event = EventUtil.getEvent(event);
    var pageX = event.pageX,
        pageY = event.pageY;
    if(pageX === undefined){
        pageX = event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft);
    }
    if(pageY === undefined){
        pageY = event.clientY + (document.body.scrollTop || document.documentElement.scrollTop);
    }
    console.log("Page coordinates:" + pageX + "," + pageY);
})

3.螢幕座標位置

screenXscreenY屬性表示滑鼠事件發生時滑鼠指標相對於整個螢幕的座標資訊。

螢幕座標:

var btn = document.getElementById('myBtn');
EventUtil.addHandler(btn,'click',function(event){
    event = EventUtil.getEvent(event);
    console.log("Page coordinates:" + event.screenX + "," + event.screenY);
})

4.修改鍵

雖然滑鼠事件主要是由滑鼠來觸發的,但在按下滑鼠時鍵盤上的某些鍵的狀態也可以影響到所要採取的操作。這些修改鍵就是ShiftCtrlAltMeta(在windows鍵盤中的windows鍵,在蘋果機中是Cmd鍵),它們經常被用來修改滑鼠事件的行為。DOM為此規定了4個屬性,表示這些修改鍵的狀態:shiftKeyctrlKeyaltKeymetaKey。這些屬性中包含的都是布林值,如果相應的鍵被按下了,則值為true,否則值為false。

當某個滑鼠事件發生時,通過檢測這幾個屬性可以確定是否使用者同時按下了其中的鍵。如下例子:

var btn = document.getElementById('myBtn');
EventUtil.addHandler(btn,'click',function(event){
    event = EventUtil.getEvent(event);
    var keys = new Array();
    if(event.shiftKey){
        keys.push('shift');
    }
    if(event.ctrlKey){
        keys.push('ctrl');
    }
    if(event.altKey){
        keys.push('alt');
    }
    if(event.metaKey){
        keys.push('meta');
    }
    console.log("keys:" + keys.join(','));
})

注:IE8以及之前的版本不支援metaKey屬性。

5.相關元素

在發生mouseovermouseout事件時,還會涉及更多的元素。這兩個事件都會涉及把滑鼠指標從一個元素的邊界之內移動到另一個元素的邊界之內。對mouseover而言,事件的主目標是獲得游標的元素,而相關元素就是那個失去游標的元素。類似地,對於mouseout事件而言,事件的主目標就是失去游標的元素,而相關元素是獲得游標的元素。來看下面一個例子:

<body>
    <div id="myDiv" style="background-color:red;width:100px;height:100px;"></div>
</body>

這個例子會在頁面上顯示一個<div>元素。如果滑鼠指標一開始就在這個<div>元素上,然後移出了這個元素,那麼就會在<div>元素上觸發mouseout事件,相關元素就是<body>元素。與此同時,<body>元素上面會觸發mouseenter事件,相關元素就變成了<div>。

DOM通過event物件的relatedTarget屬性提供了相關元素的資訊。這個屬性只對於mouseover和mouseout事件才包含值;對於其它事件,這個屬性的值為null。IE8以及之前的版本不支援relatedTarget屬性,但提供了儲存著同樣資訊的不同屬性。在mouseover事件觸發時,IE中的fromElement屬性中儲存了相關元素;在mouseout事件觸發時,IE的toElement屬性儲存著相關元素。(IE9支援這些所有屬性。)把這個新增到EventUtil物件中,如下:

var EventUtil = {
    //省略了其它程式碼
    
    //得到相關元素資訊
    getRelatedTarget:function(event){
        if(event.relatedTarget){
            return event.relatedTarget;
        }
        else if(event.toElement){
            return event.toElement;
        }
        else if(event.fromElement){
            return event.fromElement;
        }else{
            return null;
        }
    },
    
    //省略了其它程式碼
      
};

呼叫:

var myDiv = document.getElementById('myDiv');
EventUtil.addHandler(myDiv,'mouseout',function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    var relateTarget = EventUtil.getRelatedTarget(event);
    console.log("moused out of" + target.tagName + "to" + relateTarget.tagName);  //moused out ofDIVtoBODY
})

6.滑鼠按鈕

只有在主滑鼠按鈕被單擊(或鍵盤迴車鍵被按下)時才會觸發click事件,因此檢測按鈕的資訊不是必要的。但對於mousedownmouseup事件來說,則在其event物件存在一個button屬性,表示按下或者釋放的按鈕。DOM的button屬性可能有如下3個值:

  • 0:表示主滑鼠按鈕;
  • 1:表示中間的滑鼠按鈕(滑鼠滾輪按鈕);
  • 2:表示次滑鼠按鈕;

在常規的設定中,主滑鼠按鈕就是滑鼠左鍵,而次滑鼠按鈕就是滑鼠右鍵。

IE8及之前的版本也提供了button屬性,但這個屬性的值與DOM中的button屬性有很大的差異。

  • 0:沒有按下按鈕;
  • 1:表示按下了主滑鼠按鈕;
  • 2:表示按下了次滑鼠按鈕;
  • 3:表示同時按下了主次滑鼠按鈕;
  • 4:表示按下了中間的滑鼠按鈕;
  • 5:表示同時按下了主滑鼠按鈕和中間的滑鼠按鈕;
  • 6:表示同時按下了次滑鼠按鈕和中間的滑鼠按鈕;
  • 7:表示同時按下了三個滑鼠按鈕;

由於單獨使用能力檢測無法確定差異(兩種模型有同名的button屬性),因此必須另闢蹊徑。我們知道,支援DOM版滑鼠事件的瀏覽器課可以通過hasFearture()方法檢測,所有可以再為EventUtil物件新增getButton()方法:

//得到button屬性
    getButton:function(event){
        if(document.implementation.hasFeature("MouseEvents","2.0")){
            return event.button;
        }else{
            switch(event.button){
                case 0:
                case 1:
                case 3:
                case 5:
                case 7:
                    return 0;
                case 2:
                case 6:
                    return 2;
                case 4:
                    return 1;
            }
        }
    },

呼叫:

var myDiv = document.getElementById('myDiv');
EventUtil.addHandler(myDiv,'mousedown',function(event){
    event = EventUtil.getEvent(event);
    alert(EventUtil.getButton(event));
})

7.更多的事件資訊

“DOM2級事件”規範在event物件中還提供了detail屬性,用於給出有關事件的更多資訊。對於滑鼠事件來說,detail中包含了一個數值,表示在給定位置上發生了多少次單擊。在同一個畫素上相繼的發生一次mousedown和一次mouseup事件算作一次單擊。detail屬性從1開始計數,每次單擊發生後都會遞增。如果滑鼠在mousedown和mouseup之間移動了位置,則detail會被重置為0。

IE也通過下列屬性為滑鼠事件提供了更多資訊。

  • altLeft:布林值,表示是否按下了alt鍵。
  • ctrlLeft:布林值,表示是否按下了ctrl鍵。
  • offsetX:游標相對於目標元素邊界的x座標。
  • offsetY:游標相對於目標元素邊界的y座標。
  • shiftLeft:布林值,表示是否按下了shift鍵。

這些屬性用處不大,只有IE支援他們,另一方面他們提供的資訊要麼沒有什麼用,要麼可以通過其他方式計算得來。

8.滑鼠滾輪事件

IE6.0首先實現了mousewheel事件。這個事件可以在任何元素上面觸發,最終會冒泡到document(IE8)或window(IE9、Opera、Chrome及Safari)物件。與mousewheel事件對應的event物件除了包含滑鼠事件的所有標準資訊外,還包含一個特殊的wheelDelta屬性。當用戶向前滾動滑鼠滑輪時,wheelDelta是120的倍數;當用戶向後滾動滑鼠滑輪時,wheelDelta是-120的倍數。

EventUtil.addHandler(document,'mousewheel',function(event){
    event = EventUtil.getEvent(event);
    console.log(event.wheelDelta);
})

多數情況下,只需要知道滾輪的滾動方向就夠了,而這通過檢測wheelDelta的正負號就可以確定。

注意的是,在Opera9.5之前的版本中,wheelDelta值的正負號是顛倒的。如果需要支援早期的Opera版本,程式碼如下:

EventUtil.addHandler(document,'mousewheel',function(event){
    event = EventUtil.getEvent(event);
    var delta = (client.engine.opera && client.engine.opera < 9.5 ? -event.wheelDelta : event.wheelDelta);
    console.log(delta);
})

Firefox支援一個名為DOMMouseScroll的類似事件,也是在滑鼠滾輪滾動時觸發。滑鼠滾輪滾動資訊儲存在detail屬性中,當向前滾動滑鼠滾輪時,這個屬性的值為-3的整數倍,當向後滾動滑鼠滾輪時,這個屬性的值是3的整數倍。

EventUtil.addHandler(document,'DOMMouseScroll',function(event){
    event = EventUtil.getEvent(event);
    console.log(event.detail);
})

跨瀏覽器總結,新增到EventUtil物件中:

//取得滑鼠滾輪增量值(delta)
    getWheelDelta:function(event){
        if(event.wheelDelta){
            return event.wheelDelta;
        }else{
            return -event.detail * 40;
        }
    },

呼叫方式:

(function(){
    function handleMouseWheel(event){
        event = EventUtil.getEvent(event);
        var delta = EventUtil.getWheelDelta(event);
        console.log(delta);
    };
    
    EventUtil.addHandler(document,"mousewheel",handleMouseWheel);
    EventUtil.addHandler(window,"DOMMouseScroll",handleMouseWheel);
})();

9.觸控裝置

 IOS和Android的實現非常特別,因為這些裝置沒有滑鼠。在面向iphone和ipad中的Safari開發時,要記住以下幾點:

  • 不支援dblclick事件。雙擊瀏覽器視窗會放大頁面,而且沒有辦法改變該行為。
  • 輕擊可單擊元素會觸發mousemove事件。如果此操作會導致內容變化,將不再有其他事件發生;如果螢幕沒有因此變化,那麼會依次發生mousedown、mouseup和click事件。輕擊不可單擊的元素不會觸發任何事件。可單擊的元素是指那些單擊可產生預設操作的元素(如連結),或者那些已經被指定了onclick事件處理程式的元素。
  • mousemove事件也會觸發mouseover和mouseout事件。
  • 兩個手指放在螢幕上且頁面隨手指移動而滾動時會觸發mousewheel和scroll事件。

10.無障礙性問題

如果你的web應用程式或者網站要確保殘疾人特別是那些使用螢幕閱讀器的人都能訪問,那麼在使用滑鼠事件時就要格外小心。前面提到過,可以通過鍵盤上的回車鍵來觸發click事件,但其他滑鼠事件卻無法通過鍵盤來觸發。為此,我們不建議使用click之外的其他滑鼠事件來展示功能或者引發程式碼執行。因為這樣會給盲人或視障使用者造成極大不便。

鍵盤與文字事件

“DOM3級事件”為鍵盤事件制定了規範。有3個鍵盤事件如下:

  • keydown:當用戶按下鍵盤上的任意鍵時觸發,而且按住不放的話,會重複觸發該事件。
  • keypress:當用戶按下鍵盤上的字元鍵時觸發,而且按住不放的話,會重複觸發該事件。按下ESC鍵也會觸發該事件。Safari3.1之前的版本也會在使用者按下非字元鍵時觸發該事件。
  • keyup:當用戶釋放鍵盤上的鍵時觸發。

只有一個文字事件:textInput。這個事件是對keypress的補充,用意是在將文字顯示給使用者之前更容易攔截文字。在文字插入文字框之前會觸發textInput事件。

在使用者按下鍵盤上的字元鍵時,鍵盤執行順序:keydown、keypress、keyup。其中keydown、keypress都是在文字框發生變化之前被觸發的;而keyup是在文字框已經發生變化後觸發。如果使用者按下一個字元鍵不放,那麼會重複觸發keydown與keypress,直到使用者鬆開該鍵為止。

如果使用者按下的是一個非字元鍵時,執行順序:keydown、keyup。

1.鍵碼

在發生keydown與keyup事件時,event物件的keyCode屬性包含一個程式碼,與鍵盤上一個特定的鍵對應。DOM和IE中的event物件都支援keyCode屬性。如下例子:

var textbox = document.getElementById('myText');
EventUtil.addHandler(textbox,'keyup',function(event){
    event = EventUtil.getEvent(event);
    console.log(event.keyCode);
})

常用非字元鍵的鍵碼:

左箭頭:37;上箭頭:38;右箭頭:39;下箭頭40;上翻頁:33;下翻頁:34;退出(ESC):27。

無論keydown或者keyup事件都會存在一些特殊情況。在Firefox和Opera中,按分號鍵時keyCode為59,也就是ASCII中分號的編碼;但在IE,Safari,Chrome中返回186,即鍵盤中按鍵的鍵碼。

2.字元編碼

IE9、Firefox、Chrome和Safari的event物件都支援一個charCode屬性,這個屬性只有在發生keypress事件時才包含值,而且這個值是按下的那個鍵所程式碼字元的ASCII編碼。IE8及之前的版本和Opera則是在keyCode中儲存字元的ASCII編碼。下面以跨瀏覽器取得字元編碼,放在EventUtil物件中:

getCharCode:function(event){
        if(typeof event.charCode == 'number'){
            return event.charCode;
        }else{
            return event.keyCode;
        }
    },

使用方式:

var textbox = document.getElementById('myText');
EventUtil.addHandler(textbox,'keypress',function(event){
    event = EventUtil.getEvent(event);
    console.log(EventUtil.getCharCode(event));
})

在取得字元編碼後,就可以使用String.fromCharCode()將其轉化為實際的字元。如下:

var textbox = document.getElementById('myText');
EventUtil.addHandler(textbox,'keypress',function(event){
    event = EventUtil.getEvent(event);
    var charCode = EventUtil.getCharCode(event);
    var text = String.fromCharCode(charCode);
})

3.DOM3級變化

DOM3級事件中的鍵盤事件,不再包含charCode屬性,而是包含兩個新屬性:keychar,由於這個兩個屬性各瀏覽器支援程度不一樣,不推薦使用。

DOM3級事件在新增一個location屬性,也不推薦使用。

4.textInput事件

“DOM3級事件”規範中引入了一個新事件,textInput。根據規範,當用戶在可編輯區域中輸入字元時,就會觸發這個事件。keypress和textInput的區別:

  • 任何可以獲得焦點的元素都可以觸發keypress事件,但只有可編輯區域才能觸發textInput事件。
  • textInput事件只有在使用者按下能夠輸入實際字元的鍵時才會觸發,而keypress事件則在按下那些能夠影響文字顯示的鍵也會觸發(例如退格鍵)。

由於textInput事件主要考慮的是字元,因此它的event物件中還包含一個data屬性。這個屬性的值就是使用者輸入的字元(而非字元編碼)。

var textbox = document.getElementById('myText');
EventUtil.addHandler(textbox,'textInput',function(event){
    event = EventUtil.getEvent(event);
    console.log(event.data)
})

HTML5事件

1.contextmenu事件

EventUtil.addHandler(window,'load',function(event){
    var div = document.getElementById('myDiv');
     EventUtil.addHandler(div,'contextmenu',function(event){
         event = EventUtil.getEvent(event);
         EventUtil.preventDefault(event);
         
         var menu = document.getElementById('myMenu');
         menu.style.left = event.clientX + 'px';
         menu.style.top = event.clientY + 'px';
         menu.style.visibility = 'visible';
     })
     
     EventUtil.addHandler(document,'click',function(event){
         document.getElementById('myMenu').style.visibility = 'hidden';
     })
})

2.beforeunload事件

之所以有發生在window物件上的beforeunload事件,是為了讓開發人員在頁面解除安裝前阻止這一操作。可以通過這個事件來取消解除安裝並繼續使用原有頁面。

EventUtil.addHandler(window,'beforeunload',function(event){
    event = EventUtil.getEvent(event);
    var message = "你確定要離開這個頁面嗎?";
    event.returnValue = message;
    return message;
})

3.DOMContentLoaded事件

window的load事件會在頁面中的一切都載入完畢時觸發,但這個過程可能會因為要載入的外部資源過多而頗費周折。而DOMContentLoaded事件則在形成完整的DOM樹之後就會觸發,不理會影象、js檔案、css檔案或其它資源是否已經下載完畢。與load事件不同,DOMContentLoaded支援在頁面下載的早期新增事件處理程式,這也就意味著使用者能夠儘早地與頁面進行互動。

要處理DOMContentLoaded事件,可以為document或window新增相應的事件處理程式(儘管這個事件會冒泡到window,但它的目標實際上是document)。

EventUtil.addHandler(document,"DOMContentLoaded",function(event){
    alert('content loaded');
})

IE9以及其它瀏覽器支援該事件,對於不支援DOMContentLoaded的瀏覽器,建議在頁面載入期間設定一個時間為0毫秒的超時呼叫,如下:

setTimeout(function(){
    //在此新增事件處理程式
},0)

4.pageshow和pagehide事件

往返快取(back-forward cache,或bfcache),使用者使用瀏覽器的“後退”、“前進”按鈕加快頁面的轉換速度。將整個頁面儲存在記憶體裡。

(function(){
    var showCount = 0;
    EventUtil.addHandler(window,'load',function(){
        alert('loaded fired')
    });
    
    EventUtil.addHandler(window,"pageshow",function(event){
        showCount++;
        alert("Show has been fired" + showCount + "times.");
    });
})()

裝置事件

1.orientationchange事件

2.MozOrientation事件

3.deviceorientation事件

4.devicemotion事件

觸控與手勢事件

觸控事件如下:

  • touchstart:當手指觸控式螢幕幕時觸發。
  • touchmove:當手指在螢幕滑動時連續的觸發。在這個事件發生期間,可以呼叫preventDefault()可以阻止滾動。
  • touchend:當手指從螢幕上移開時觸發。
  • touchcancel:當系統停止跟蹤觸控時觸發。

上面這幾個事件都會冒泡,也都可以取消。它們是以相容DOM的方式實現的。因此,每個觸控事件的event物件都提供了在滑鼠事件中常見的屬性:bubbles 、cancelabel 、view 、clientX 、clientY 、screenX、 screenY 、detail 、altKey、 ctrlKey、 shiftKey 和metaKey。

除了常見的DOM屬性外,觸控事件還包括下列三個用於跟蹤觸控的屬性。

  • touches:表示當前跟蹤的觸控操作的Touch物件的陣列。
  • targetTouchs:特定於事件目標的Touch物件的陣列。
  • changedTouches:表示自上次觸控以來發生了什麼改變的Touch物件的陣列。

每個touch物件包含下列屬性:

  • clientX:觸控目標在視口中的x座標。
  • clientY:觸控目標在視口中的y座標。
  • identifier:標識觸控的唯一ID。
  • pageX:觸控目標在頁面中的x座標。
  • pageY:觸控目標在頁面中的y座標。
  • screenX:觸控目標在螢幕中的x座標。
  • screenY:觸控目標在螢幕中的y座標。
  • target:觸控的DOM節點目標。

如下面例子:

 

function handlerTouchEvent(event){
    
    //只跟蹤一次觸控
    if(event.touches.length == 1){
        var output = document.getElementById('output');
        switch(event.type){
            case "touchstart":
                output.innerHTML = "Touch started(" + event.touches[0].clientX + "," + event.touches[0].clientY + ")";
                break;
            case "touchend":
                output.innerHTML += "<br/>Touch ended(" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")";
                break;
            case "touchmove":
                event.preventDefault(); //阻止滾動
                output.innerHTML += "<br/>Touch moved(" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")";
                break;
        }
    }
}

EventUtil.addHandler(document,"touchstart",handlerTouchEvent);
EventUtil.addHandler(document,"touchend",handlerTouchEvent);
EventUtil.addHandler(document,"touchmove",handlerTouchEvent);

 注意,在touchend事件發生時,touches集合中就沒有任何的touch物件了,因為不存在活動的觸控操作;此時就必須轉而使用changedTouches集合。

在觸控式螢幕幕上的元素時,這些事件(包括滑鼠事件)發生的順序如下:

  • 1.touchstart
  • 2.mouseover
  • 3.mousemove(一次)
  • 4.mousedown
  • 5.mouseup
  • 6.click
  • 7.touched

目前只有IOS版的Safari支援多點觸控。

記憶體和效能

在JS中,新增到頁面上的事件處理程式數量將直接關係到頁面的整體執行效能。導致這一問題的原因是多方面的。首先,每個函式都是物件,都會佔用記憶體;記憶體中的物件越多,效能就越差。其次,必須事先指定所有事件處理程式而導致的DOM訪問次數,會延遲整個頁面的互動就緒時間。事實上,從如何利用好事件處理程式的角度出發,還是有一些辦法能夠提升效能的。

事件委託

事件委託利用事件冒泡,只指定一個事件處理程式,就可以管理某一型別的所有事件。例如,click事件會冒泡到document層次。也就是說,我們可以為整個頁面新增一個onclick事件處理程式,而不必給每個可單擊的元素分別新增事件處理程式。

如下程式碼,點選3個li分別執行不同的操作:

 html程式碼:

<ul id="myLinks">
    <li id="goSomewhere">Go somewhere</li>
    <li id="doSomething">Do something</li>
    <li id="sayHi">Say hi</li>
</ul>

JS程式碼:

var list = document.getElementById('myLinks');
EventUtil.addHandler(list,'click',function(event){
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    switch(target.id){
        case "goSomewhere":
            document.title = "I changed the document's title";
            break;
        case "doSomething":
            window.location.href = "https://www.baidu.com";
            break;
        case "sayHi":
            alert('hi');
            break;
    }
})

 移除事件處理程式

html:

<div id="myDiv">
    <input type="button" value="click me" id="myBtn"/>
</div>

JS:

var btn = document.getElementById('myBtn');
btn.onclick = function(){
    //先執行某些操作
    btn.onclick = null;
    document.getElementById('myDiv').innerHTML = 'Processing...';
}

我們在設定<div>的innerHTML屬性之前,先移除了按鈕的事件處理程式。這樣就確保了記憶體可以被再次利用。

注意,在事件處理程式中刪除按鈕也能阻止事件冒泡。目標元素在文件中是事件冒泡的前提。

模擬事件

DOM中的事件模擬

可以在document物件上使用createEvent()方法建立event物件。這個方法接收一個引數,表示要建立的事件型別的字串。在DOM2級中,所有這些字串都使用英文複數形式,而在DOM3級中都變成了單數。這個字串可以是下列字串之一:

  • UIEvents:一般化的UI事件。滑鼠事件和鍵盤事件都繼承自UI事件。DOM3級是UIEvent。
  • MouseEvents:一般化的滑鼠事件。DOM3級是MouseEvent。
  • MutationEvents:一般化的DOM變動事件。DOM3級中是MutationEvent。
  • HTMLEvents:一般化的HTML事件。沒有對應的DOM3級事件(HTML事件被分散到了其他類別中)。

在建立了event物件後,還需要使用與事件有關的資訊對其進行初始化。每種型別的event物件都有一個特殊的方法,為它傳入適當的資料就可以初始化該event物件。不同型別的方法的名字也不相同,這個取決於createEvent()中使用的引數。

事件模擬的最後一步就