深刻理解JavaScript 事件

  • 本文總結自《JavaScript高級程序設計》以及本身平時的經驗,針對較新瀏覽器以及 DOM3 級事件標準(2016年8月),對少部份內容做了更正,增長了各類例子及解析。
  • 如無特殊說明,本文後的文字引用和圖片引用均來自《JavaScript高級程序設計》,引用稍有改變原文,不改變意思。
  • 本文僅做鞏固基礎之用,若是有不正確的地方,還望指出。
  • 更好的排版能夠點這裏

事件

我的認爲:不管是瀏覽器自帶的事件,仍是自定義事件,都是觀察者模式的實現。更確切地說:事件流是會流動的,流到哪一個節點,事件在哪裏發生,事件發生時,節點便會調用在這個節點綁定的事件處理程序。節點是被觀察者,事件處理程序是觀察者,當事件流流到被觀察者時,被觀察者會對外宣稱「我這裏發生了某個事件」,即通知觀察者,也就是節點調用事件處理程序。事件流是不知道被觀察者有多少個的,因此即便是0個,事件流也會繼續流,流到節點時,節點會遍歷本身註冊的事件處理程序,存在就調用。具體瀏覽器的實現和優化確定更加複雜和精妙,但原理應該是這樣(以上爲我的理解)。javascript

事件流

事件流分爲事件冒泡和事件捕獲:css

  • 若是你把手指放在圓心上,那麼你的手指指向的不是一個圓,而是紙上的全部圓。在瀏覽器上單擊按鈕的同時,你也單擊了按鈕的容器元素,甚至也單擊了整個頁面。事件流描述的是從頁面中接收事件的順序
  • IE開發團隊提出了事件冒泡流、Netscape開發團隊提出了事件捕獲流。

事件冒泡

  • 事件開始時由最具體的元素(文檔中嵌套層次最深的那個節點)接收,而後逐級向上傳播到較爲不具體的節點,全部現代瀏覽器都支持事件冒泡,除IE5.5外,均一直冒泡到window。
  • 事件冒泡示意圖:

    事件冒泡示意圖

事件捕獲

  • 不太具體的節點應該更早接收到事件,而最具體的節點應該最後接收到事件。事件捕獲的用意在於在事件到達預約目標以前捕獲它。IE9+、Safari、Chrome、Opera和Firefox支持,且從window開始捕獲(儘管DOM2 級事件規範要求從document)。
  • 事件捕獲示意圖:

    事件捕獲示意圖
  • 因爲老版本的瀏覽器不支持,所以不多有人使用事件捕獲。咱們也建議讀者放心地使用事件冒泡,在有特殊須要時再使用事件捕獲
  • 爲了完全理解事件冒泡和捕獲,這裏寫了個例子:html

    <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>test1</title> <link rel="stylesheet" href="test1.css"> </head> <body> <div id="a"> <div id="b"> <div id="c"></div> </div> </div> <script src="test1.js"></script> </body> </html>
#a{ width: 300px; height: 300px; background: pink; } #b{ width: 200px; height: 200px; background: blue; } #c{ width: 100px; height: 100px; background: yellow; }
var a = document.getElementById("a"), b = document.getElementById("b"), c = document.getElementById("c"); c.addEventListener("click", function(event){ console.log("c1") // 注意第三個參數沒有傳進 false , 由於默認傳進來的是 false,表明冒泡階段調用,我的認爲處於目標階段也會調用的 }); c.addEventListener("click", function(event){ console.log("c2"); }, true); b.addEventListener("click", function(event){ console.log("b"); }, true); a.addEventListener("click", function(event){ console.log("a1"); }, true); a.addEventListener("click", function(event){ console.log("a2") }); a.addEventListener("click", function(event){ console.log("a3"); event.stopImmediatePropagation(); }, true); a.addEventListener("click", function(event){ console.log("a4"); }, true);
  • 效果如圖
    事件流示例html5

  • 點擊 c 或 b,輸出:a一、a3
  • stopImmediatePropagation 包含了 stopPropagation 的功能,即阻止事件傳播(捕獲或冒泡),但同時也阻止該元素上後來綁定的事件處理程序被調用,因此不輸出 a4,由於事件捕獲被攔截了,天然不會觸發 b、c 上的事件,因此不輸出 b、c一、c2,冒泡更談不上了,因此不輸出 a2。有人會以爲上面的表述有一點點問題,爲何捕獲被攔截了,c1 就不輸出了呢? c1 應該是冒泡階段被調用的呀,因此應該改成另外一個表述:「...冒泡更談不上,因此不輸出 c一、a2」。但另外一個表述是錯的,下面會分析到。
  • 點擊 a,輸出 a一、a二、a3
  • 不該該是 a一、a三、a2 嗎?a一、a3 但是在捕獲階段被調用的處理程序啊,a2 是在冒泡階段被調用的啊。這裏正是要說明的:雖然這三個事件處理程序註冊時指定了true、false,但如今事件流是處於目標階段,不是冒泡階段、也不是捕獲階段,事件處理程序被調用的順序是註冊的順序。不論你指定的是 true or false. 這也解釋了上面提到的「另外一種表述」爲何是錯誤的。
  • 更深一步解釋是:要區分事件流和事件處理程序,不論事件處理程序存不存在,事件流都會傳播。這是一個觀察者模式,綁定事件的節點是被觀察者、事件處理程序是觀察者,事件流是不知道觀察者的存在的,因此你點擊頁面的時候,事件流必定要傳播,傳播到某一個節點時,節點去通知全部觀察者,也就是調用事件處理程序(有可能觀察者不存在)。
  • 當一個事件流來到一個節點時,事件流可能在捕獲階段(正在流向最深層次的節點)、可能在處於目標階段(已經流到了目標,也就是event.target)、也可能在冒泡階段(正在流向最外層節點)。而事件處理程序是這麼處理的:① 註冊時第三個參數指定爲 true 時,事件流到來,若是事件流是捕獲階段或處於目標階段,則調用該事件處理程序。②註冊時第三個參數指定爲 false 時,當事件流到來,若是事件流是處於目標階段或冒泡階段,則調用該事件處理程序。
  • 因此當事件流是處於目標階段,那麼無論事件處理程序第三個參數指定的true or false,事件處理程序都會被調用,調用順序按照註冊順序。因此點擊a,輸出 a一、a二、a3,而不是a一、a三、a2。
  • 註釋掉 event.stopImmediatePropagation,點擊 c,輸出 a一、a三、a四、b、c一、c二、a2java

  • 另外,若是同一個事件處理程序(指針相同,好比用 handler 保存的事件處理程序),用 addEventListener 或 attachEvent 綁定屢次,若是第三個參數是相同的話,也只會被調用一次。但若是第三個參數一個設置爲true,另外一個設置爲false,那麼會被調用兩次。git

DOM事件流

  • 「DOM2級事件」規定的事件流包括三個階段:事件捕獲階段、處於目標階段和事件冒泡階段。首先發生的是事件捕獲,爲截獲事件提供了機會。而後是實際的目標接收到事件。最後一個階段是冒泡階段。(事件處理中「處於目標階段」被當作冒泡階段的一部分)。
  • IE九、Safari、Chrome、Firefox和Opera9.5及更高版本都會在捕獲階段觸發事件對象上的事件,就是有兩個機會在目標對象上面操做事件。(儘管DOM2級事件規範明確要求捕獲階段不涉及事件目標)。

事件處理程序

HTML 事件處理程序

簡單來說,HTML 事件處理程序是直接在HTML中綁定事件,以下github

<input type="button" value="Click Me" onclick="alert(&quot;Clicked&quot;)" />

注意事項:

  • 不能在其中使用未經轉義的HTML語法字符,如&「」<>,由於這是在HTML中綁定的,會形成瀏覽器解析DOM結構錯誤。
  • 擴展函數做用域,來看下面的代碼:web

    <!-- 輸出 "Click Me、lzh" --> <form method="post"> <input type="text" name="username" value="lzh"> <input type="button" value="Click Me" onclick="alert(value);alert(username.value);"> </form>

    若是當前元素是一個表單輸入元素,瀏覽器內部大概是這樣實現的:chrome

    function () { with (document) { with (this.form) { with (this) { //元素屬性值 } } } }
    若是沒有form元素,調用username會報錯,因此不管是服務端渲染仍是Ajax請求回來數據再渲染,最好仍是把form結構寫完整。
    擴展做用域有三個缺點:
  1. 函數被調用時還沒定義會報錯,只好try{}catch(ex){},分離的寫法能夠在DOMContentLoaded以後再綁定。
  2. 擴展的做用域鏈在不一樣瀏覽器中會致使不一樣的結果。
  3. HTML 與 JavaScript 代碼緊密耦合,若是要更換事件處理程序,須要改動 HTML 代碼和 JavaScript代碼。

DOM0級事件處理程序

  • 每一個元素(包括window 和document)都有本身的事件處理程序屬性,這些屬性一般所有小寫。使用 DOM0 級指定的事件處理程序被認爲是元素的方法。this 引用當前元素。經過 this 能夠訪問元素的任何屬性和方法。DOM0 級事件處理程序在冒泡階段被處理。
var btn = document.getElementById("myBtn"); btn.onclick = function () { alert(this.id); //"myBtn" };

DOM2級事件處理程序

  • addEventListener() 包含三個參數,要處理的事件名、事件處理函數、布爾值,布爾值爲true,表示在捕獲階段調用事件處理程序,反之在冒泡階段調用。
  • DOM2 級事件處理程序中的 this 也指向 addEventListener 的那個元素。
  • 能夠添加多個事件處理程序,按添加順序依次調用。
  • removeEventListener 沒法移除匿名函數的事件處理程序。
var btn = document.getElementById("myBtn"); var handler = function () { alert(this.id); }; btn.addEventListener("click", handler, false); //這裏省略了其餘代碼 btn.removeEventListener("click", handler, false); // 有效!
  • IE九、Firefox、Safari、Chrome 和Opera 支持DOM2 級事件處理程序。

IE事件處理程序

  • attachEvent detachEvent 接收兩個參數,事件處理程序名稱、事件處理程序函數。因爲IE8及更早版本只支持事件冒泡,因此該事件處理程序只支持事件冒泡。
  • 老版本的Opera支持這種方法,但如今Opera已經改用blink內核,IE11已經不支持這種方法,注意 IE9 就已經支持 DOM2 級事件處理程序了。
  • 特別要注意:第一個參數包含on,好比onclick。
  • 區別於DOM0 級事件處理程序,this 指向 'window'。
  • 也能夠添加多個事件處理程序。

跨瀏覽器的事件處理程序

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; } } };
  • 存在問題:
  1. IE事件處理程序 中的 this 指向 window
  2. 只支持 DOM0 級的瀏覽器不能屢次添加事件處理程序,不過這種瀏覽器應該很少了,即便是IE8 也支持attachEvent。
  3. 會不會有一些事件,在瀏覽器支持 DOM2 級事件處理程序的狀況下,那些事件只能用 on + name 的形式呢? 以前一直懷疑 (1).xhr.onreadystatechange() 和 (2).DOMNodeInserted 事件,這裏我多慮了,通過驗證,(1).是支持 DOM2 級事件的,(2).天生就是 DOM2 級的。這裏只是爲了打消個人疑慮,記錄下來。

事件對象

DOM 中的事件對象

  • 兼容 DOM 的瀏覽器會將一個 event 對象傳入事件處理程序, IE9 及更高版本能夠。不管指定事件處理程序時使用什麼方法(DOM0 級 DOM2 級),HTML 事件處理程序能夠經過訪問 event 變量獲得 event 對象。
  • event 中的屬性和方法都是隻讀的
  • 經常使用屬性:
  1. target 事件的目標
  2. currentTarget 綁定事件的元素,與 'this' 的指向相同
  3. stopPropagation() 取消事件的進一步捕獲或冒泡。若是bubbles爲true,則可使用這個方法
  4. stopImmediatePropagation() 取消事件的進一步捕獲或冒泡,同時阻止任何事件處理程序被調用(DOM3級事件中新增)
  5. preventDefault() 取消事件的默認行爲,好比點擊連接跳轉。若是 cancelable 是 true,則可使用這個方法
  6. type 被觸發的事件類型
  7. eventPhase 調用事件處理程序的階段:1表示捕獲階段,2表示「處於目標」,3表示冒泡階段
  • this target currentTarget 舉例:
document.body.onclick = function(event){ alert(event.currentTarget === document.body); //true alert(this === document.body); //true alert(event.target === document.getElementById("myBtn")); //true };
  • 經過 event.type 與 switch case 組合,能夠經過一個函數處理多個事件。
  • 只有在事件處理程序執行期間,event 對象纔會存在;一旦事件處理程序執行完成,event 對象就會被銷燬。

IE 中的事件對象

  • DOM0 級的事件處理程序,event 做爲 window 的一個屬性存在。(從 IE9 開始,event 能夠從參數中得到)
  • attachEvent 添加的事件處理程序,event 做爲參數傳入,也能夠經過 window 來訪問 event 對象。
  • HTML 事件處理程序依然能夠經過訪問 event 變量獲得 event 對象。
  • 屬性和方法:
  1. cancelBubble 設置 true or false 能夠取消事件冒泡
  2. returnValue 設置 true or false 能夠取消事件的默認行爲。
  3. srcElement 事件的目標(與DOM中的 target 相同)
  • 注意事項:
  1. attachEvent 中的 event.srcElement === this 嗎? 答案是否認的,由於前面說到過 attachEvent 中 this 指向 window, DOM0 級、DOM2 級 事件處理程序 this 才指向 event.target / window.event.srcElement

跨瀏覽器的事件對象

var EventUtil = { getEvent: function(event){ return event ? event : window.event; // window.event DOM0級時IE }, getTarget: function(event){ return event.target || event.srcElement; // event.srcElement for IE }, preventDefault: function(event){ if (event.preventDefault){ event.preventDefault(); } else { event.returnValue = false; // IE } }, stopPropagation: function(event){ if (event.stopPropagation){ event.stopPropagation(); } else { event.cancelBubble = true; // IE } } };

事件類型

  • DOM3 級事件規定了幾類事件;HTML5 也定義了一組事件;還有一些事件沒有規範,瀏覽器的實現不一致。
  • DOM3 級事件模塊在 DOM2 級事件模塊基礎上從新定義了這些事件,也添加了一些新事件。包括 IE9 在內的全部主流瀏覽器都支持 DOM2 級事件。IE9 也支持 DOM3 級事件。

這裏只總結一些常見的事件類型數組

UI事件類型

  • load 事件,當頁面徹底加載後(包括全部圖像、JavaScript 文件、CSS 文件等外部資源),就會觸發 window 上面的 load 事件。
EventUtil.addHandler(window, "load", function(){ var image = document.createElement("img"); EventUtil.addHandler(image, "load", function(event){ event = EventUtil.getEvent(event); alert(EventUtil.getTarget(event).src); }); document.body.appendChild(image); image.src = "smile.gif"; //在此以前要先指定事件處理程序 });
  1. script 元素也會觸發 load 事件,據此能夠判斷動態加載的 JavaScript 文件是否加載完畢。與圖像不一樣,只有在設置了 script 元素的 src 屬性並將該元素添加到文檔後,纔會開始下載 JavaScript 文件
  2. IE8 及更早版本不支持 script 元素上的 load 事件。
  3. 在不屬於 DOM 文檔的圖像(包括未添加到文檔的 img 元素和 Image 對象)上觸發 load 事件時,IE8 及以前版本不會生成 event 對象。IE9 修復了這個問題。
  • resize 事件
  1. 瀏覽器窗口大小發生變化時會觸發該事件,這個事件在 window 上觸發,IE、Safari、Chrome 和 Opera 會在瀏覽器窗口變化了 1 像素時就觸發 resize 事件,而後隨着變化不斷重複觸發。Firefox 則只會在用戶中止調整窗口大小時纔會觸發。
  2. 注意不要在這個事件的處理程序中加入大計算量的代碼,或者採用函數節流的方式優化性能。
  3. 瀏覽器窗口最小化或最大化時也會觸發 resize 事件。
  • scroll 事件
  1. 該事件在 window 上發生,此處和書上講的有點不同,webkit 內核或 blink 內核的瀏覽器(Chrome、Opera、Safari)能夠經過 document.body.scrollTop 獲取頁面被捲去的高度,而 Trident、Gecko (IE、火狐)能夠經過 document.documentElement.scrollTop來獲取該值。
  2. 另外標準模式、混雜模式這兩種方法還有出入,此處不討論。
  3. 因此最好經過 document.body.scrollTop + document.documentElement.scrollTop 的方式獲取 scrollTop 的值,由於二者之一會等於0,或者使用 document.body.scrollTop || document.documentElement.scrollTop,二者效果一致。

焦點事件

  1. 這裏忽略 DOMFocusIn、DOMFocusOut,由於只有 Opera 支持這個事件,且 DOM3 級事件廢棄了它們。
  2. blur:在元素失去焦點時觸發。這個事件不會冒泡;全部瀏覽器都支持它。
  3. focus:在元素得到焦點時觸發。這個事件不會冒泡;全部瀏覽器都支持它。
  4. focusin:與 focus 等價,但它冒泡。
  5. focusout:與 blur 等價,也冒泡。
  6. 支持 focusin、focusout 的瀏覽器有:IE5.5+、Safari 5.1+、Opera 11.5+和Chrome。但只支持 DOM2 級事件處理程序
  7. Firefox 不支持 focusin、focusout
  8. blur、focusout 的事件目標是失去焦點的元素;focus、focusin 的事件目標是得到焦點的元素

鼠標與滾輪事件

  • click 在用戶單擊住鼠標按鈕或按下回車鍵時觸發。 觸發順序 mousedown mouseup click,若是 mousedown、mouseup 其中之一被取消,就不會觸發 click 事件。
  • dblclick 觸發順序 mousedown mouseup click mousedown mouseup click dblclick, 若是中間有事件被取消,dblclick 也不會被觸發
  • mousedown 用戶按下了任意鼠標按鈕時觸發。
  • mouseup 用戶釋放按鈕時觸發
  • mouseenter 在鼠標光標從元素外部首次移動到元素範圍以內時觸發。不冒泡,並且在光標移動到後代元素上不會觸發。DOM2 級事件並無定義這個事,但 DOM3 級事件將它歸入了規範。IE、Firefox9+和Opera支持這個事件。
  • mouseleave 在位於元素上方的鼠標光標移動到元素範圍以外時觸發。不冒泡,並且在光標移動到後代元素上不會觸發。DOM2 級事件並無定義這個事,但 DOM3 級事件將它歸入了規範。IE、Firefox9+ 和 Opera 支持這個事件。
  • mouseover 在鼠標指針位於一個元素外部,而後用戶將其首次移入另外一個元素邊界以內時觸發。不能經過鍵盤觸發這個事件。
  • mouseout 在鼠標指針位於一個元素上方,而後用戶將其移入另外一個元素時觸發。又移入的另外一個元素可能位於前一個元素的外部,也多是這個元素的子元素。不能經過鍵盤觸發這個事件。
  • 用代碼說明一下 mouseenter、mouseleave 和 mouseover、mouseout 的區別:
<!DOCTYPE html> <html lang="zh-cn"> <head> <title>test1</title> <meta charset="utf-8"> <link rel="stylesheet" type="text/css" href="test1.css"> </head> <body> <div class="mouseover"> <div class="sub-mouseover"> </div> </div> <div class="mouseenter"> <div class="sub-mouseenter"> </div> </div> <script src="test1.js"></script> </body> </html>
.wrap { width: 200px; height: 100px; } .mouseover { background: pink; } .mouseenter { margin-top: 30px; background: gray; } .sub-mouseover, .sub-mouseenter { width: 100px; height: 50px; background: #AE81FF; }
var div1 = document.querySelector(".mouseover"), div2 = document.querySelector(".mouseenter"); div1.addEventListener("mouseover", function(){ console.log("div1 mouseover"); }); div1.addEventListener("mouseout", function(){ console.log("div1 mouseout"); }) div2.addEventListener("mouseenter", function(){ console.log("div2 mouseenter"); }) div2.addEventListener("mouseleave", function(){ console.log("div2 mouseleave"); })
  • 效果圖
    mouseenter-mouseover

  • 鼠標由左側從上到下依次通過全部 div 的狀況,輸出 div1 mouseover div1 mouseout div1 mouseover div1 mouseout div2 mouseenter div2 mouseleave

  • mousemove 當鼠標指針在元素內部移動時重複地觸發。不能經過鍵盤觸發這個事件。
  • 除了 mouseenter、mousedleave,全部鼠標事件都會冒泡,取消鼠標事件將會影響瀏覽器的默認行爲,也會影響其它事件,由於鼠標事件與其它事件是密不可分的。
  • 關於 dblclick IE8 及以前版本中的實現有一個小bug,所以在雙擊事件中,會跳過第二個mousedown 和click事件,其順序以下:mousedown mouseup click mouseup dblclick,但仍是會觸發 dblclick 事件
  • 客戶區座標位置:鼠標事件中的 event 都有 clientX clientY 屬性,表示在視口中客戶區的座標位置,這些值不包括頁面滾動的距離,所以這個位置並不表示鼠標在頁面上的位置:
    座標示例
  • 頁面座標位置:pageX、pageY,這兩個屬性表示鼠標光標在頁面中的位置,在頁面沒有滾動的狀況下,pageX 和 pageY 的值與 clientX、clientY 的值相等。IE8 及更早版本不支持事件對象上的頁面座標,不過使用客戶區座標和滾動信息能夠計算出來。這時候須要用到document.body(混雜模式)或document.documentElement(標準模式)中的scrollLeft 和scrollTop 屬性。計算過程以下所示:
var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "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); } alert("Page coordinates: " + pageX + "," + pageY); });
  • 屏幕座標位置:screenX、screenY
  • 修改鍵 用戶按住Shift、Ctrl、Alt、Meta(Windows或Cmd,cmd(mac))時觸發鼠標事件,能夠在 event 中得到修改鍵。
var div = document.getElementById("myDiv"); EventUtil.addHandler(div, "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"); } alert("Keys: " + keys.join(",")); });
  • IE九、Firefox、Safari、Chrome 和Opera 都支持這4 個鍵。IE8 及以前版本不支持metaKey 屬性。另外,舊版本的 IE 有本身的一套寫法。
  • 相關元素 mouseover mouseout 時的 event.relatedTarget,不作詳細記錄。
  • 鼠標按鈕 mousedown mouseup 是在按下/釋聽任意鼠標按鈕時觸發的,因此經過 event.button: 0(左) 1(中) 2(右) 能夠判斷按的是哪一個鍵,可是IE8 及更低版本的瀏覽器不支持,有兼容寫法,此處不詳細敘述。EventUtil.getButton 有詳細實現。
  • mousewheel event.whellDelta 爲正數時,向前滾動(回到頂部、頁面向下滑動),負數則反過來,這個值是120的倍數,Opera低版本中正負相反,火狐中有本身的一套方法,這裏不作詳細記錄。
  • 觸摸設備
  • 不支持dblclick 事件。雙擊瀏覽器窗口會放大畫面,並且沒有辦法改變該行爲。
  • 輕擊可單擊元素會觸發mousemove 事件。若是此操做會致使內容變化,將再也不有其餘事件發生;若是屏幕沒有所以變化,那麼會依次發生mousedown、mouseup 和click 事件。輕擊不可單擊的元素不會觸發任何事件。可單擊的元素是指那些單擊可產生默認操做的元素(如連接),或者那些已經被指定了onclick 事件處理程序的元素。
  • mousemove 事件也會觸發mouseover 和mouseout 事件。
  • 兩個手指放在屏幕上且頁面隨手指移動而滾動時會觸發mousewheel 和scroll 事件。
  • 無障礙性問題
  • 若是須要考慮這個問題,不建議使用 click 以外的鼠標事件。由於這個不能經過鍵盤觸發,不利於屏幕閱讀器訪問。此處不詳細記錄。

鍵盤與文本事件

  • keydown: 當用戶按下鍵盤上的任意鍵時觸發,並且若是按住不放的話,會重複觸發此事件。
  • keypress 當用戶按下鍵盤上的字符鍵時觸發,並且若是按住不放的話,會重複觸發此事件。按下Esc 鍵也會觸發這個事件。Safari 3.1 以前的版本也會在用戶按下非字符鍵時觸發keypress事件。
  • keyup:當用戶釋放鍵盤上的鍵時觸發。
  • 觸發順序:keydownkeypresskeyupkeydownkeypress 都是在文本框發生變化以前被觸發的; keyup 事件則是在文本框已經發生變化以後被觸發的。
  • 若是用戶按下了一個字符鍵不放,就會重複觸發 keydown 和keypress 事件,直到用戶鬆開該鍵爲止。
  • 鍵盤事件也支持修改鍵(ctrl等)
  • keydown、keyup 中的 event 有 keyCode, 與ASCII 碼中對應小寫字母或數字的編碼相同。
  • keypress 中的 event 有 charCode,這個值是按下的那個鍵所表明字符的 ASCII 編碼,用 String.fromCharCode() 能夠轉換成實際的字符
  • DOM3 級中,有 key 和 char,其中 key 能夠直接獲得 "k"、"K"、"Shift" 等, char 屬性在按下字符鍵時行爲與 key 相同,在按下非字符鍵時爲 null,可是支持還不完整,chrome 老是輸出 undefined。
  • keyIdentifier Chrome 已經不推薦使用
  • 表示按下的按鍵在鍵盤的位置,好比按下左右側的shift鍵,這個值就不一樣,Chrome 和 Safari 的實現有 bug。
  • textInput: 在文本插入文本框以前會觸發textInput 事件。目的是代替keypress,退格鍵不會觸發textInput,可是會觸發keypress(只要改變文本),只有真正能夠編輯的區域纔會觸發textInput,可是keypress得到焦點便可觸發。event.data中包含用戶的輸入,拼音輸入法中輸入過程的拼音不會觸發該事件。
  • inputMethod 表明用戶是怎樣輸入的,好比經過粘貼的方式,可是支持的瀏覽器不多。

變更事件

DOM2 級的變更(mutation)事件能在 DOM 中的某一部分發生變化時給出提示,好比 DOM 節點的插入、移除、特性被修改等等

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"; }); });
  1. beforeunload 事件,用戶關閉標籤頁時提示
EventUtil.addHandler(window, "beforeunload", function(event){ event = EventUtil.getEvent(event); var message = "I'm really going to miss you if you go."; event.returnValue = message; return message; });
  1. DOMContentLoaded 在造成完整DOM樹以後就會觸發,不理會圖像、JavaScript 文件、CSS 文件或其它資源是否已經下載完畢。其實更應該使用 DOMContentLoaded 而不是 window.onload:
EventUtil.addHandler(window, "DOMContentLoaded", function(event){ alert("Content loaded."); }); EventUtil.addHandler(window, "load", function(event){ alert("Window loaded."); });
  • IE9+、Firefox、Chrome、Safari 3.1+ 和 Opera9+ 都支持 DOMContentLoaded 事件。
  1. readystatechange 事件,略。
  2. pageshow 和 pagehide 事件,此處要了解 Firefox 和 Opera 有一個特性叫 「往返緩存」(back-forward cache/bfcache),用戶點擊「前進」、「後退」按鈕時,會將頁面緩存在內存。不從新加載,JavaScript的狀態會保留。可是不管頁面是否來自 bfcache,都會觸發 pageshow 事件,pageshow 的事件處理程序的 event 對象中有 event.persisted 屬性,爲 true 表明頁面來自bfcache,一樣 pagehide 事件觸發時,若是頁面被保存到 bfcache 中,則該屬性爲 true。支持pageshow、pagehide 事件的瀏覽器有 Firefox、Safari5+、Chrome 和 Opera。 IE9 及之前的版本不支持這兩個事件。指定了 onunload 事件處理程序的頁面會被自動排除在 bfcache 以外。
  3. hashchange 事件。在 window 上觸發,event 包含 oldURL、newURL 兩個屬性。支持該事件的有 IE8+、Firefox3.6+、Safari5+、Chrome 和 Opera10.6+,但oldURL、newURL只有Firefox6+、Chrome和Opera支持。因此最好用 location 來指定當前的 hash:
EventUtil.addHandler(window, "hashchange", function(event){ console.log(location.hash); })

設備事件

  • orientationchange 事件,屏幕轉動。

觸摸與手勢事件

  • touchstart: 當手指觸摸屏幕時觸發;即便已經有一個手指放在了屏幕上也會觸發。
  • touchmove: 當手指在屏幕上滑動時連續地觸發。在這個事件發生期間,調用preventDefault() 能夠阻止滾動。
  • touchend:當手指從屏幕上移開時觸發。
  • touchcancel:當系統中止跟蹤觸摸時觸發。關於此事件的確切觸發時間,文檔中沒有明確說明。
  • event 對象中包含的常見 DOM 屬性有:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey 和metaKey。
  • event 對象中還包含如下用於跟蹤觸摸的屬性:
  1. touches:表示當前跟蹤的觸摸操做的Touch 對象的數組。
  2. targetTouchs:特定於事件目標的Touch 對象的數組。
  3. changeTouches:表示自上次觸摸以來發生了什麼改變的Touch 對象的數組。每一個Touch 對象包含下列屬性:clientX、clientY、pageX、pageY、screenX、screenY、target、identifier(標識觸摸的惟一ID)
function handleTouchEvent(event) { //only for one touch 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(); //prevent scrolling output.innerHTML += "<br>Touch moved (" + event.changedTouches[0].clientX + "," + event.changedTouches[0].clientY + ")"; break; } } }
  • 一次觸摸的事件觸發順序爲:touchstart、mouseover、mousemove(一次)、mousedown、mouseup、click、touchend
  • 手勢事件:
  1. gesturestart:當一個手指已經按在屏幕上而另外一個手指又觸摸屏幕時觸發。
  2. gesturechange:當觸摸屏幕的任何一個手指的位置發生變化時觸發。
  3. gestureend:當任何一個手指從屏幕上面移開時觸發。
  • 屬性有標準的鼠標事件屬性,還有兩個:rotation(正值表示順時針)和scale(從1開始)

內存和性能

  • 每一個函數都是對象,都會佔用內存;內存中的對象越多,性能就越差。
  • 必須事先指定全部事件處理程序而致使的 DOM 訪問次數,會延遲整個頁面的交互就緒時間。

事件委託

<body> <ul id="myLinks"> <li id="goSomewhere">Go somewhere</li> <li id="doSomething">Do something</li> <li id="sayHi">Say hi</li> </ul> <script type="text/javascript"> (function(){ var list = document.getElementById("myLinks"); EventUtil.addHandler(list, "click", function(event){ event = EventUtil.getEvent(event); var target = EventUtil.getTarget(event); switch(target.id){ case "doSomething": document.title = "I changed the document's title"; break; case "goSomewhere": location.href = "http://www.wrox.com"; break; case "sayHi": alert("hi"); break; } }); })(); </script> </body>
  • 上面的方法只取得了一個 DOM 元素,只添加了一個事件處理程序,佔用的內存更少。
  • 若是將事件委託到 document 中,會更有優點:
  1. document 對象很快就能夠訪問,並且能夠在頁面生命週期的任什麼時候點上爲它添加事件處理程序(無需等待 DOMContentLoaded 或 load 事件)。
  2. 在頁面中設置事件處理程序所需的時間少。只添加一個事件處理程序所需的 DOM 引用更少,所花的時間也更少。
  3. 整個頁面佔用的內存空間更少,可以提高總體性能。
  • 最適合採用事件委託技術的事件包塊 clickmousedownmouseupkeydownkeyup 和 keypress

移除事件處理程序

  • 若是你知道某個元素即將被移除,那麼最好手工移除事件處理程序,由於有的瀏覽器(尤爲是 IE)不會做出恰當地處理,它們頗有可能會將對元素和對事件處理程序的引用都保存在內存中。
  • IE8 及更早的版本在頁面被卸載(刷新,切換頁面)以前沒有清理乾淨事件處理程序,它們會滯留在內存中,能夠經過 onunload 事件處理程序移除全部事件處理程序。

模擬事件

  • 在測試 Web 應用程序,模擬觸發事件是一種極其有用的技術。DOM2 級規範爲此規定了模擬特定事件的方式,IE九、Opera、Firefox、Chrome 和 Safari 都支持這種方式。IE有它本身模擬事件的方式(IE8 及如下才要用到)

DOM 中的事件模擬

  • 能夠在 document 對象上使用 createEvent 方法建立 event 對象。這個方法接收一個參數,即表示要建立的事件類型的字符串。在 DOM2 級中,全部這些字符串都使用英文複數形式,而在 DOM3 級中變成了單數。這個字符串能夠是下列幾個字符串之一:
  1. UIEvents,DOM3 級中是 UIEvent
  2. MouseEvents: 通常化的鼠標事件,DOM3 級中是 MouseEvent
  3. MutationEvents: 通常化的 DOM 變更事件。 ...
  4. HTMLEvents 通常化的 HTML 事件。沒有對應的 DOM3 級事件(HTML 事件被分割到其餘類別中)

模擬鼠標事件

  • createEvent 方法返回的 event 對象中,有 initMouseEvent() 方法,須要傳 15 個參數。type(好比"click"),bubbles(Boolean) 是否冒泡,應該設置爲 true, cancelable(Boolean) 應該設置爲 true,view(幾乎老是document.defaultView), detail(一般設置爲0), screenX, screenY, clientX, clientY, ctrlKey, altKey, shiftKey, metaKey, button(表示按下了哪一個鼠標,默認0), relatedTarget(只有在模擬 mouseover 或 mouseout時使用)
  • 將 event 對象傳給 DOM 節點的 dispatchEvent 方法便可觸發事件,以下:
<body> <input type="button" value="Click me" id="myBtn" /> <input type="button" value="Send click to the other button" id="myBtn2" /> <p>This example works in DOM-compliant browsers (not IE).</p> <script type="text/javascript"> (function(){ var btn = document.getElementById("myBtn"); var btn2 = document.getElementById("myBtn2"); EventUtil.addHandler(btn, "click", function(event){ alert("Clicked!"); alert(event.screenX); //100 }); EventUtil.addHandler(btn2, "click", function(event){ //create event object var event = document.createEvent("MouseEvents"); //initialize the event object event.initMouseEvent("click", true, true, document.defaultView, 0, 100, 0, 0, 0, false, false, false, false, 0, btn2); //fire the event btn.dispatchEvent(event); }); })(); </script> </body>

模擬鍵盤事件

  • "DOM2 級事件"的草案中原本包含了鍵盤事件,但在定稿前又被刪除了;Firefox 根據其草案實現了鍵盤事件。但跟 "DOM3 級事件"中的鍵盤事件有很大區別。
  • DOM3 級規定,調用 createEvent() 並傳入 "KeyboardEvent" ,返回鍵盤事件,有 initKeyEvent() 方法。這個方法接收一下參數
  • type, bubbles, cancelable, view, key(按下的鍵的鍵碼), location(按下了哪裏的鍵,0:主鍵盤,1:左,2:右,3:數字鍵盤,4:虛擬鍵盤,5:手柄), modifiers: 空格分隔的修改鍵列表,如 "Shift", repeat(在一行中按了這個鍵多少次)
    DOM3 級不提倡 keypress 事件, 所以只能模擬 keydown keyup

IE 中的事件模擬

第一步:document.createEventObject()
第二步: 經過賦值的方式初始化事件對象,就是 event.screenX = 0 這些
第三步:btn.fireEvent("onclick", event);

關於標準

    • 因爲標準在變,如今 DOM3 級事件已經不推薦使用 document.createEvent 的方式,也不推薦經過 event 對象 initKeyEvent或者 initKeybordEvent,書中的跨瀏覽器代碼在狐火中報錯了,由於火狐開始支持 DOM3 級事件,標準又在變,如今 DOM3 級標準推薦經過構造函數的方式初始化模擬事件,但這也仍是草案。

    • 關於跨瀏覽器模擬事件,粗略瞭解一下 jQuery 的作法,使用了不少 hack,讓原本不冒泡的 focus、blur 能夠作事件委託,裏面的內容仍是不少,得另外總結一下。
    • 期待標準被普及的一天:

相關文章
相關標籤/搜索