js06-js事件、事件委託、冒泡和捕獲!

事件

簡述事件

  • 事件起始於IE3,作爲一種分擔服務器運算負載的一種手段。用於鍵盤、鼠標等工具對於網頁的交互!事件對於不同瀏覽器來說,有不同的標準,尤其是IE、Chrome兩大巨頭瀏覽器上,雖然現如今Chrome已經佔據大部分市場,但是對於IE8及以上的兼容也是個不小的問題。

事件類型

  • UI事件:用戶與頁面上的元素交互時觸發;
  • 焦點事件:當元素獲取失去焦點是觸發;
  • 鼠標事件:當鼠標執行點擊、移入移出或懸停等事件是觸發;
  • 滾輪事件:當鼠標、觸摸板或其他設備滾動時觸發滾動條事件;
  • 文本事件:在文本框內輸入文字時觸發;
  • 鍵盤事件:當鍵盤按下、擡起或點擊時觸發;
  • 變動事件:當問文檔結構發生變化時觸發。

UI事件

  • load事件

    當頁面完全加載後再window上面觸發,當所有框架都加載完成時,在框架集上面觸發,當圖片都加載完成時在img標籤上面觸發,或者當嵌入的內容加載完成時在object上觸發。

  • unload事件

    和load事件恰好相反,除img標籤外,其他的框架和嵌入內容卸載完畢後在對應的級別上觸發。

  • abort事件

    在用戶停止下載過程時,如果嵌入的內容沒有加載完畢,在object元素上線觸發。

  • error事件

    當js發生語法錯誤時在window上觸發,當發生加載圖像無法成功時在img標籤上觸發,當嵌入的內容無法加載時在object元素上觸發,或當一個或多個框架無法加載完成時在框架集上面觸發。

  • select事件

    當用戶選擇文本框內的文字時觸發(input或textarea)。

  • resize事件

    當窗口或者框架的大小變化時在window或框架上面觸發。

  • scroll事件

    當用戶滾動帶有滾動條的元素中的內容時,在該元素上面觸發。body上就是網頁自帶的滾動條。

對於文檔嵌入內容不是很清楚的同學可以去看一下HTML5權威指南的第15章節內容。

焦點事件

  • blur事件、focusout

    輸入框失去焦點事件,css有相同僞類選擇器,但是不夠靈活!

  • focusin事件、focus

    輸入框獲取焦點是觸發,同樣,css有相同的僞類事件。輸入框如果使用 click同樣能獲取焦點,但是有點牛頭馬嘴的意思。

  • focus和blur事件不支持冒泡,如果不想文檔事件冒泡則使用這兩個事件更好,不用屏蔽事件流。

鼠標與滾輪事件

  • mousedown、mouseup事件

    鼠標左鍵按下、擡起事件,相繼觸發這兩個事件後還會觸發click事件。

  • click事件、dbclick事件

    鼠標單擊、雙擊事件

  • mouseleave、mouseout事件

    都是鼠標脫離當前區域觸發,不同的是mouseleave不冒泡。

  • mouseenter、mouseover事件

    鼠標進入、懸停在當前區域時觸發,mouseenter事件不冒泡。

  • mousemove事件

    當鼠標在當前區域移動時重複觸發!

  • mousewheel事件

    滾輪事件在鼠標中間滾動時觸發,同時包含一個wheelData屬性,它是120的整數倍數,滾輪向上滾動一個單位時wheelDate+120,向下滾動時-120,。

鍵盤與文本事件

  • keydown:當用戶按下鍵盤等按鍵設備上的按鈕時觸發,按住不放會重複觸發。
  • keyup、keypress:按鍵按下後釋放時觸發。兩個事件都是同時觸發keyCode的值,但是keypress會輸出input框的上一個值,而keyup會立即輸出當前input的值。如下:
<body>
    <input type="text" id="test">
</body>
<script>
    /* 雖然id可以直接拿來使用,但是並不提倡,這不符合規範! */
    test.onkeypress=function(event){ //event是系統默認傳遞的事件對象
        console.log(event.keyCode); //輸出鍵碼
        console.log(test.value); //輸出input值
    };
</script>

在我依次按下數字鍵1、2、3、4、5、6時控制檯是如下效果:

index.html:15    49
index.html:16    
index.html:15    50
index.html:16    1
index.html:15    51
index.html:16    12
index.html:15    52
index.html:16    123
index.html:15    53
index.html:16    1234
index.html:15    54
index.html:16    12345

但是如果是keyup的事件呢:

<body>
    <input type="text" id="test">
</body>
<script>
    /*雖然id可以直接拿來使用,但是並不提倡,這不符合規範!*/
    test.οnkeyup=function(event){
        console.log(event.keyCode);
        console.log(test.value);
    };
</script>

在我依次按下數字鍵1、2、3、4、5、6時控制檯是如下效果:

index.html:15    49
index.html:16    1
index.html:15    50
index.html:16    12
index.html:15    51
index.html:16    123
index.html:15    52
index.html:16    1234
index.html:15    53
index.html:16    12345
index.html:15    54
index.html:16    123456

事件流

  • 事件流描述的是事件在網頁文檔中的接收順序,它描述着事件發生時,事件是順着文檔流向上還是向下!
  • 但是奇趣的是,IE和Netscape開發團隊提出了兩個完全相反的事件流概念!那就是事件冒泡和事件捕獲!

根據網頁文檔的DOM級別規定層級結構:

文檔結構
document
html
body
element

事件冒泡

假定事件在element上觸發,然後事件會跟着下圖一次冒泡

graph LR
Element-->body
body-->html
html-->document

事件是默認冒泡的,在IE9,Opera9.5及其他瀏覽器各版本及更高版本都是支持事件流的,但是上述明確瀏覽器的更低版本則不支持。

<body>
    <div id="testElement" style="background-color: greenyellow;width: 100px;height: 100px;"></div>
</body>
<script>
    /* 雖然id可以直接拿來使用,但是並不提倡,這不符合規範! */
    window.onclick = function () {
        console.log(this);
    };
    document.onclick = function () {
        console.log(this);
    };
    document.body.parentNode.onclick = function () {
        console.log(this);
    };
    document.body.onclick = function () {
        console.log(this);
    };
    testElement.onclick = function () {
        console.log(this);
    };
</script>

控制檯輸出的結果:

index.html:29   <div id="testElement" style="background-color: greenyellow;width: 100px;height: 100px;"></div>
index.html:26    <body>…</body>
index.html:23    <html lang="en"><head>…</head><body>…</body></html>
index.html:20    #document
index.html:17    Window{postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window,…}

如果我們把this全價格toString方法改爲字符串!

index.html:29    [object HTMLDivElement]
index.html:26    [object HTMLBodyElement]
index.html:23    [object HTMLHtmlElement]
index.html:20    [object HTMLDocument]
index.html:17    [object Window]

事件捕獲

同事件冒泡正好相反,它會按照事件流向下級結構傳遞:

graph LR
document-->html
html-->body
body-->element

而控制他們事件流傳遞方向的創建事件監聽時的一個參數,後面我再詳細解釋。

創建和移除事件監聽

創建事件監聽addEventListener()方法

testElement.addEventListener("click",function(event) {
        console.log(this);
        console.log(event.type);
},false);
/*
index.html:32    <div id"testElement" style="background-color: greenyellow;width: 100px;height: 100px;"></div> 
index.html:33    click
*/

它的第三個參數默認爲false,代表默認使用事件冒泡規則,同時事件會根據冒泡規則一次向上傳遞,如果改爲true則事件採用事件捕獲規則,根據文檔結構和元素關係進行事件的向下傳遞。

事件的移除removeEventListener()方法

testElement.removeEventListener("click",function(event) {
        console.log(this);
        console.log(event.type);
},false);

但是我們要注意的地方是,事件移除程序移除的是第二參數傳遞進去的函數,如果是函數體,會因爲指向問題使移除監聽並不算成功,而我們如果傳遞進去的是函數名的話,則能很好的達到效果!沒聽懂,沒關係,看下面:

testElement.addEventListener("click",function(event) { //它沒有被取消掉
        console.log(this);
        console.log(event.type);
},false);
testElement.removeEventListener("click",function(event) { //它沒有效果
        console.log(this);
        console.log(event.type);
},false);

/*
index.html:32 <div id="testElement" style="background-color: greenyellow;width: 100px;height: 100px;"></div>
index.html:33 click
*/

沒錯,remove裏的沒有執行,但是add裏的卻仍然很好的執行了!那我們來看正確的方式!


testElement.addEventListener("click", handler, false);
testElement.removeEventListener("click", handler, false);

function handler(event) {
    console.log(this)
    console.log(event.type);
}
/*
控制檯你什麼也不會看到!
*/

究其原因,是因爲上面的創建和移除是各自創建的函數,它們看起來很像,但是根本沒有絲毫"血緣"關係,它們在內存中佔據了兩個不同的位置,而後面引用的函數名確實代表的同一個"東西",它們指向的是內存中的同一個位置!!!

阻止事件的冒泡或者捕獲、默認行爲

  • 阻止事件流的傳遞,stopPropagation()方法,爲了方便我這裏不用規範的創建監聽方法了:
<body>
    <div id="testElement" style="background-color: greenyellow;width: 100px;height: 100px;"></div>
</body>
<script>
    /* 雖然id可以直接拿來使用,但是並不提倡,這不符合規範! */
    window.onclick = function () {
        console.log(this);
    };
    document.onclick = function () {
        console.log(this);
    };
    document.body.parentNode.onclick = function () {
        console.log(this);
    };
    document.body.onclick = function () {
        console.log(this);
    };
    testElement.onclick = function (event) {
        console.log(this);
        console.log(event.type);
        event.stopPropagation(); //阻止了事件冒泡
    };
</script>

/*
index.html:29    <div id="testElement" style="background-color: greenyellow;width: 100px;height: 100px;"></div>
index.html:30    click
*/

如果要測試事件捕獲需要用規範的創建事件監聽,並使第三個參數改爲true就可以了!

  • 阻止默認行爲發生

    說到默認行爲,就像a標籤點擊了會默認跳轉到href一樣,那麼我們可以通過使用preventdefault()方法實現,但是前提條件是對象的屬性cancelable屬性必須設置爲true纔可以阻止該對象的默認行爲,使用時同時用event對象來調用!大家可以自行測試,這裏就不贅述了。

事件委託

  • 內存和性能:在一門編程語言中,給一個控件或者目標添加看似是司空見慣的事,但是卻有着非比尋常的。在包含GUI編程的語言裏,給每一個控件添加事件都是沒有問題的,因爲它編譯的時候有一些特殊的事情要做,像我學過的C#/.NET中就是這樣,不過js和衆多桌面語言中有着很大的卻別,因爲js是個單線程的語言,不能像桌面語言那樣new Thread()來繼續做自己的事,而是必須等待!事件在整個編譯運行過程中也是遵循排隊等待的鐵則!那麼精簡的事件監聽程序則是一個js腳本是否性能良好的體現!

跨瀏覽器的事件監聽程序

  • 說起來高大上,說實在的就是因爲瀏覽器對事件監聽程序創建的差異,谷歌是addEventListener(),IE 是attachEvent(),或者其他瀏覽器支持用οnclick=handler(以點擊事件爲例)創建,而對於事件,谷歌可以直接用click,而IE就必須用onclick等等,爲了js程序能在不同瀏覽器都能跑起來,所以,一個整合的跨瀏覽器的事件監聽創建程序非常必要!畢竟還有有很多不會升級電腦和軟件的朋友存在,尤其那些還在用xp系統的辦公電腦啊。。。
var EventUtil = {
    addHandler: function (ele, type, handler) {
        if (ele.addEventListener) {
            ele.addEventListener(type, handler, false);
        } else if (ele.attachEvent) {
            ele.attachEvent("on" + type, handler);
        } else {
            ele["on" + type] = handler;
        }
    },
    removeHandler: function (ele, type, handler) {
        if (ele.removeEventListener) {
            ele.removeEventListener(type, handler, false);
        } else if (ele.detachEvent) {
            ele.detachEvent("on" + type, handler);
        } else {
            ele["on" + type] = null;
        }
    },
    getEvent: function (event) {
        return event ? event : window.event; //低版本IE的event對象位置不同!
    },
    getTarget: function (event) {
        return event.target || event.srcElement;//不同瀏覽器有不同的調取方法
    },
    preventDefault: function (event) {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.retrunValue = false;
        }
    },
    stopPropagation:function(event){
        if(event.stopPropagation){
            event.stopPropagation();
        }else{
            event.cancelBubble();
        }
    }
};
//比如創建一個鍵盤監聽事件
EventUtil.addHandler(document, "keyup", function (event) {
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    KeyUp(target, event.keyCode ? event.keyCode.toString() : "mouse");
});

什麼是事件委託?

  • 剛纔我有提到,js本就是一個簡單的腳本語言,沒有桌面編程語言的多線程,所以每一個對象都創建一個甚至多個事件監聽是非常耗費性能的!那麼我們怎麼解決呢?就是事件委託,事件委託的中心思想就是,我給當前模塊儘可能給最大的父級創建事件監聽程序,然後通過父級對子級進行事件的分發,對子級擁有對應功能的事件進行響應!形象化就像是一個酒店的服務檯,一個服務檯發送一個點擊事件,服務檯將事件分發到需要觸發該事件的對應房間,因爲js的單線程機制,所有消息和事件都是在隊列中排隊的,所以沒有“佔線”這一說法!所有的事件都能正常地分發給對應目標,從而省下很多性能資源!
  • 實現以下委託:
/*
事件處理程序創建了一個鍵盤監聽事件,通過EventUtil對象的getEvent獲取當前事件,然後通過event的特有屬性,
event.target獲取到事件觸發時的響應目標(直接獲取到元素對象!),然後通過參數等調用相應需要實現的函數
*/
EventUtil.addHandler(document, "keyup", function (event) {
    event = EventUtil.getEvent(event);
    var target = EventUtil.getTarget(event);
    KeyUp(target, event.keyCode ? event.keyCode.toString() : "mouse");
});
  • 這裏我把鍵盤事件放在document上,是爲了鍵盤能在全局被監聽到,使用時並不是對象越大越好,一般來說模塊化裏同一個子父級樹裏儘量按照更高的父級關係設置是最好的!
  • 以點擊事件爲例,假設我有兩個不相干的盒子A和盒子B在同一個容器div盒子C裏,它們各有自己的子級input框,button,div,a標籤等,那麼我的事件委託就可以通過對A或B的監聽,將事件分發給自己的子級,事件委託對象並不是越大越好!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章