瀏覽器事件解析

JavaScript、瀏覽器、事件之間的關係

JavaScript程序採用了異步事件驅動編程(Event-driven programming)模型,維基百科對它的解釋是:javascript

事件驅動程序設計(Event-driven programming)是一種電腦程序設計模型。這種模型的程序運行流程是由用戶的動做(如鼠標的按鍵,鍵盤的按鍵動做)或者是由其餘程序的消息來決定的。相對於批處理程序設計(batch programming)而言,程序運行的流程是由程序員來決定。批量的程序設計在初級程序設計教學課程上是一種方式。然而,事件驅動程序設計這種設計模型是在交互程序(Interactive program)的狀況下孕育而生的html

簡言之,在web前端編程裏面JavaScript經過瀏覽器提供的事件模型API和用戶交互,接收用戶的輸入。前端

因爲用戶的行爲是不肯定的。這種場景是傳統的同步編程模型無法解決的,由於你不可能等用戶操做完了才執行後面的代碼。因此在javascript中使用了異步事件,也就是說:js中的事件都是異步執行的java

事件驅動程序模型基本的實現原理基本上都是使用 事件循環(Event Loop),這部份內容涉及瀏覽器事件模型、回調原理。node

JavaScript DOM、BOM模型中,一樣異步的還有setTimeout,XMLHTTPRequest這類API並非JavaScript語言自己就有的。程序員

事件綁定的方法

事件綁定有3種方法:web

行內綁定

直接在DOM元素上經過設置on + eventType綁定事件處理程序。例如:chrome

<a href="#none" onclick="alert('clicked')">點擊我</a>

這種方法有兩個缺點:編程

  1. 事件處理程序和HTML結構混雜在一塊兒,不符合MVX的規範。爲了讓內容、表現和行爲分開,咱們應該避免這種寫法。
  2. 這樣寫的代碼判斷具備全局做用域,可能會產生命名衝突,致使不可預見的嚴重的後果。

在DOM元素上直接重寫事件回調函數

使用DOM Element上面的on + eventType屬性 APIsegmentfault

var el = getElementById('button');  //button是一個<button>元素
el.onclick = function(){ alert('button clicked.') };
el.onclick = function(){ alert('Button Clicked.') };
//實際之彈出'Button Clicked.',函數發生了覆蓋

這種方法也有一個缺點:後綁定的函數會覆蓋以前的函數。好比咱們註冊一個window.onload事件,可能會覆蓋某個庫中已有的事件函數。固然,這個能夠有解決方法:

function addEvent(element, EventName, fun) {   //EventName = 'on' + eventType
    var oldFun = element[EventName];
    if (typeof oldFun !== 'function') {
        element[EventName] = fun;
    } else {
        element[EventName] = function() {
            oldFun();
            fun();
        };
    }
}

addEvent(window, "onload", function() { alert('onload 1') });
addEvent(window, "onload", function() { alert('onload 2') });

固然,通常狀況下使用DOM Ready就能夠了,由於JavaScript在DOM加載完就能夠執行了

標準綁定方法

標準的綁定方法有兩種,addEventListener和attachEvent前者是標準瀏覽器支持的API,後者是IE8如下瀏覽器支持的API:

//例如給一個button註冊click事件
var el = getElementById('button');  //button是一個<button>元素
if(el.addEventLister){
    el.addEventListener("click", function(e){
        alert("button clicked.");
    },false);
}
if(el.attachEvent){
    el.attachEvent("onclick", function(e){
        alert("button clicked.");
    });
}

須要注意的是:

  1. addEventLister的第一個參數事件類型是不加on前綴的,而attachEvent中須要加on前綴。
  2. addEventLister中的事件回調函數中的this指向事件元素target自己,而attachEvent中的事件回調函數的this指向的是window。
  3. addEventLister有第三個參數,true表示事件工做在捕獲階段,false爲冒泡階段(默認值:false)。而attachEvent只能工做在冒泡階段。

在chrome中運行以下代碼:

<a href="javascript:alert(1)" onclick="alert(2)" id="link">click me</a>
<script>
    var link = document.getElementById('link');
    link.onclick = function() { alert(3); };  //覆蓋了行內的onclick定義
    link.addEventListener('click', function() { alert(4); },false);
    link.addEventListener('click', function() { alert(5); },false);
</script>

點擊後彈出順序是: 3 -> 4 -> 5 -> 1

這裏第4行代碼覆蓋了行內的onclick定義,若是註釋了這一行,輸入順序爲: 2 -> 4 -> 5 -> 1,而addEventListener之間不會發生覆蓋。

解除事件綁定

對於上述的前二個方法,解除事件綁定只須要將對應的事件函數設爲null,就能夠了:

var el = document.getElementById('button');
el.onclick = null;

對於上述第三種方法使用removeListen()方法便可,在IE8中,對應使用detachEvent()。注意,他們和上面的註冊方法一一對應,不能混用。

//這是一段錯誤代碼,不能實現事件移除

//創建一個事件
var el = document.getElementById('button');  //button是一個<button>元素
if(el.addEventLister){
    el.addEventListener("click", function(e){
        alert("button clicked.");
    },false);
}
if(el.attachEvent){
    el.attachEvent("onclick", function(e){
        alert("button clicked.");
    });
}

//試圖移除這個事件
if(el.removeEventLister){
    el.addEventListener("click", function(e){
        alert("button clicked.");
    },false);
}
if(el.detachEvent){
    el.datachEvent("onclick", function(e){
        alert("button clicked.");
    });
}

//移除失敗

以上的錯誤在於事件函數這樣定義時,雖然看着徹底同樣,但在內存中地址不同。這樣一來,電腦不會認爲解除的和綁定的是同一個函數,天然也就不會正確解除。應該這樣寫:

//創建一個事件
var el = document.getElementById('button');  //button是一個<button>元素
var handler = function(e){alert("button clicked.");};
if(el.addEventLister){
    el.addEventListener("click", handler,false);
}
if(el.attachEvent){
    el.attachEvent("onclick", handler);
}

//試圖移除這個事件
if(el.removeEventLister){
    el.addEventListener("click", handler, false);
}
if(el.detachEvent){
    el.datachEvent("onclick", handler);
}

//移除成功

事件的捕獲與冒泡

以前說addEventListener函數的第三個參數表示捕獲和冒泡,這個是一個重點!

我本身描述一下他們的定義就是:

冒泡:在一個元素上觸發的某一事件,會在這個元素的父輩元素上會依次由內向外觸發該事件,直到window元素。

捕獲:在一個元素上觸發的某一事件,這個元素的每一層的全部子元素上觸發該事件,並逐層向內,直到全部元素再也不有子元素。

以下圖(注:圖片來自百度搜索)

事件間回到函數參數是一個事件對象,它裏面包括許多事件屬性和方法,好比,咱們能夠用如下方式阻止冒泡和默認事件:

//該例子只寫了handler函數
function handler(event) {
    event = event || window.event;
    //阻止冒泡
    if (event.stopPropagation) {
        event.stopPropagation();      //標準方法
    } else {
        event.cancelBubble = true;    // IE8
    }
    //組織默認事件
    if (event.perventDefault) {
        event.perventDefault();      //標準方法
    } else {
        event.returnValue = false;    // IE8
    }
}

其次,普通註冊事件只能阻止默認事件,不能阻止冒泡

element = document.getElemenById("submit");
element.onclick = function(e){
    /*...*/
    return false;    //經過返回false,阻止冒泡
}

事件對象

事件函數中有一個參數是事件對象,它包含了事件發生的全部信息,好比鍵盤時間會包括點擊了什麼按鍵,包括什麼組合鍵等等,而鼠標事件會包括一系列屏幕中的各類座標和點擊類型,甚至拖拽等等。固然,它裏面也會包括不少DOM信息,好比點擊了什麼元素,拖拽進入了什麼元素,事件的當前狀態等等。

這裏關於事件兼容性有必要強調一下:

document.addEventListener('click', function(event) {
    event = event || window.event;   //該對象是註冊在window上的
    console.log(event);   //能夠輸出事件對象看一看, 屬性不少不少
    var target = event.target || event.srcElement;  //前者是標準事件目標,後者是IE的事件目標
},false);

關於鼠標事件座標的問題,能夠看另外一篇博客:元素和鼠標事件的距離屬性

事件觸發

除了用戶操做之外,咱們也能夠寫代碼主動觸發一個事件,以ele元素的click事件爲例:

ele.click();   //觸發ele元素上的單擊事件

事件代理

有時候咱們須要給不存在的的一段DOM元素綁定事件,好比用戶動態添加的元素,或者一段 Ajax 請求完成後渲染的DOM節點。通常綁定事件的邏輯會在渲染前執行,但綁定的時候找不到元素因此並不能成功。

爲了解決這個問題,咱們一般使用事件代理/委託(Event Delegation)。並且一般來講使用 事件代理的性能會比單獨綁定事件高不少,咱們來看個例子。

  • 傳統註冊事件方法,當內容不少時效率低,不支持動態添加元素
<ul id="list">
    <li>item-1</li>
    <li>item-2</li>
    <li>item-3</li>
    <li>item-4</li>
    <li>item-5</li>
</ul>
<script>
    var lists = document.getElementsByTagName('li');
    for(var i = 0; i < lists.length; ++i){
        lists[i].onclick = (function(i){
                return function(){
                  console.log("item-" + (i + 1));
                };
            })(i);
    }
    //添加節點
    var list = document.getElementById('list');
    var newNode = document.createElement('li');
    newNode.innerHTML = "item-6";
    list.appendChild(newNode);
</script>
  • 事件委託註冊方法,不論內容有多少都只註冊1次,支持動態添加元素:
<ul id="list">
    <li>item-1</li>
    <li>item-2</li>
    <li>item-3</li>
    <li>item-4</li>
    <li>item-5</li>
</ul>
<script>
    var list = document.getElementById('list');
    var handler = function(e){
      e = e || window.event;
      var target = e.target || e.srcElement;
      if(target.nodeName && target.nodeName === "LI"){
        console.log(target.innerHTML);
      }
    };

    if(list.addEventListener){
      list.addEventListener("click", handler);
    } else {
      list.attachEvent("onclick", handler);
    }

    //添加節點
    var list = document.getElementById('list');
    var newNode = document.createElement('li');
    newNode.innerHTML = "item-6";
    list.appendChild(newNode);
</script>

事件封裝

很明顯,處理瀏覽器兼容太麻煩了,因此這裏把js中的事件註冊相關函數封裝一下,做爲整理。

//均採用冒泡事件模型
var myEventUtil={
    //添加事件函數
    addEvent: function(ele, event, func){
        var target = event.target || event.srcElement;
        if(ele.addEventListener){
            ele.addEventListener(event, func, false);
        } else if(ele.attachEvent) {
            ele.attachEvent('on' + event, func);   //func中this是window
        } else {
            ele['on' + event] = func;    //會發生覆蓋
        }
    },
    //刪除事件函數
    delEvent:function(ele, event, func) {
        if(ele.removeEventListener){
            ele.removeEventListener(event, func, false);
        } else if(ele.detachEvent) {
            ele.detachEvent('on' + event, func);
        } else {
            ele['on' + event] = null;
        }
    },
    //獲取觸發事件的源DOM元素
    getSrcElement: function(event){
        return event.target || event.srcElement;
    },
    //獲取事件類型
    getType: function(event){
        return event.type;
    },
    //獲取事件
    getEvent:function(event){
        return event || window.event;
    },
    //阻止事件冒泡
    stopPropagation: function(event) {
        if(event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBuble = false;
        }
    },
    //禁用默認行爲
    preventDefault: function(event){
        if(event.preventDefault){
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    }
};

jQuery中的事件

須要注意的是: JQuery中的事件都工做在冒泡階段,且只能工做在冒泡階段

註冊、解除事件

  • 方法一:
//不會發生覆蓋,但不利於解除,不能動態操做事件
<button id="button">here</button>
$("#button").click(function(){   //註冊一個click事件,固然能夠用其餘事件名的函數註冊其餘事件
  console.log("clicked");
});
  • 方法二:
//不會發生覆蓋,利於解除,不能動態操做事件
<button id="button">here</button>
//註冊一個事件
$("#button").bind("click", function() {    //註冊一個click事件,固然能夠用其餘事件名的函數註冊其餘事件
  console.log("clicked");
});
//固然還能夠這樣寫,給事件指定命名空間
$(document).bind('click.handler1', function() { console.log(1);})
$(document).bind('click.handler2', function() { console.log(2);})

//解除一個事件
$("#button").unbind(".handler1");    //解除元素上因此handler1命名空間中的事件
$("#button").unbind('click.handler2');   // 解除元素上的click.handler2事件
$("#button").unbind('click');            // 解除元素上全部點擊事件
$("#button").unbind()                    // 解除元素上全部事件

//bind()方法還介受3個參數形式,這裏就不贅述了,感興趣能夠本身看看相關資料。
  • 方法三:
//不會發生覆蓋,但不利於解除,能動態操做事件,依賴於事件冒泡

//註冊事件
$(document).delegate(".item", "click", function(){console.log(this.innerHTML);});   //第一個是選擇器, 第二個是事件類型, 第三個是事件函數

//移除事件
$(document).undelegate(".item", "click", handler);  //移除元素上指定事件
$(document).undelegate(".item", "click");  //移除元素上全部click事件
$(document).undelegate(".item");  //移除元素上全部事件
  • 方法四:
//不會發生覆蓋,但不利於解除,能動態操做事件,不依賴於事件冒泡

//註冊事件
#(".item").live("click", function(){console.log(this.innerHTML);})  //第一參數是事件類型, 第二參數是事件函數

//移除事件
$(".item").die("click", handler);  //移除元素上指定click事件
$(".item").die("click");  //移除元素上全部click事件
  • 兩個簡化方法:
//hover方法
$("#button").hover(function(){
        //鼠標移入時的動做,不冒泡
    }, function(){
        //鼠標移出時的動做,不冒泡
});

//toggle方法
$("#button").toggle(function(){
        //第一次點擊時的動做
    }, function(){
        //第二次點擊時的動做
}, .../*能夠放多個函數,依次循環響應*/);

事件觸發

//不能觸發addEventListener和attachEvent
//主動觸發一個事件
$("#button").trigger("click");   //觸發全部click事件
$("#button").trigger("click.handler1");   //觸發全部click.handler1事件
$("#button").trigger(".handler1");   //觸發全部handler1命名空間的事件
$("#button").trigger("click!");   //觸發全部沒有命名空間的click事件
$("#button").trigger(event);   //在該元素上觸發和事件event同樣的事件
$("#button").trigger({type:"click", sync: true});   //觸發click事件,同步
相關文章
相關標籤/搜索