Native DOM Event Module

我是在看 tap.js 的程式碼時,才真的注意到 DOM 的 event module (事件模組)其實我們已經可以拿來用了,關鍵在於比較少人注意到的 dispatchEvent 這個 method,這個 method 可以讓你對任意的 DOM Node 觸發任意事件,不管是 click 事件、mousemove 事件,還是 keydown 事件都可以,所以理論上你可以用 JS 模擬所有使用者做的操作,要做 integration 測試也沒問題,不過像是滑鼠拖拉要模擬就有點辛苦了。而除了原生有的事件外,其實 dispatchEvent 還支援自訂事件,對於自訂事件用途還不清楚的可以先看看以前的文章

dispatchEvent 的用法和一般 JavaScript Library 的 trigger event 不太一樣,要先建立一個 event 物件給他:

var event = document.createEvent('Event');
event.initEvent(type, true, true);

domnode.dispatchEvent(event);

其中的 type 才是你想要觸發的事件名稱,而第一個 createEvent 收的參數,除了 "Event" 這個最通用的之外,還有很多種原生事件,如果是要自訂事件的話,可以用最通用的 "Event"。不過其實,還有一個比較新的 API:

var event = new CustomEvent(type, {bubbles: true, cancelable: true});

這是 DOM Level 4 裡面提出來的 Custom Event Constructor,在這之前的 Level 3 就可以用第一個範例的方法產生 custom event,要先 create 再 init。這邊岔題一下,應該有人會注意到,createEvent 後 initEvent 那邊的 API 設計和 CustomEvent 不一樣,initEvent 收三個值,後面兩個都傳了 true,可是這樣的 API 設計近年來都被認為是不好的,因為你看程式碼,你完全不知道這兩個 true 是什麼意思,所以都是建議用第二種 API 的設計,收一個 options 物件,其實,initEvent 那兩個 true 分別就是 CustomEvent 後面的 bubbles 和 cancelable,但是後者的設計就可以讓程式碼看一眼就知道那兩個 true 是什麼用途的,看到這種不止開發者的程式在進化,連標準也跟著一起演進的現象還蠻有趣的~

Custom Event Constructor 目前 IE 的支援還比較差,所以還不能直接使用,還是要做一下 feature detection。至於哪些物件可以使用這些 method,除了 DOM Node,外,其實一般瀏覽器都還有一些物件可以聽事件,像是 window、document、 XMLHttpRequest instance 等,很可惜不是任意物件都可以,事實上,這些可以使用 DOM Event Module 的物件都是實作了 EventTarget 這個介面,不過又要再說一次很可惜的,目前沒辦法把瀏覽器內的這些實作偷出來給其它物件使用,幸好 Andrea Giammarchi 有做了 一個 放在 Github 上,可以給一般物件繼承去用。

我曾經寫過一篇文章介紹 addEventListener 的第三個參數,這篇文章的最後我要來介紹第二個參數,第二個參數一般都是丟 function 進去,不過他其實也可以丟 EventListener 進去,基本上就是一個物件有 handleEvent 來當事件的 callback 而已,不過像 tap.js 就有很有趣的用法,它不管是什麼事件,在加上 addEventListener 時丟進去的都是 this 物件,實際上則是統一在 handleEvent 裡面在根據事件的不同去作不同的事情

Tap.prototype.handleEvent = function (e) {
    switch (e.type) {
        case 'touchstart': this.start(e); break;
        case 'touchmove': this.move(e); break;
        case 'touchend': this.end(e); break;
        case 'touchcancel': this.cancel(e); break;
        case 'mousedown': this.start(e); break;
        case 'mousemove': this.move(e); break;
        case 'mouseup': this.end(e); break;
    }
};