jQuery 源碼系列(十二)事件體系結構

歡迎來個人專欄查看系列文章。javascript

前面一章,大概是一個總覽,介紹了事件綁定的初衷和使用,經過了解,知道其內部是一個什麼樣的流程,從哪一個函數到哪一個函數。不管 jQuery 的源碼簡單或者複雜,有一點能夠確定,jQuery 致力於解決瀏覽器的兼容問題,最終是服務於使用者。css

一些遺留問題

前面介紹 bind、delegate 和它們的 un 方法的時候,經提醒,忘記提到一些內容,倒是咱們常用的。好比 $('body').click$('body').mouseleave等,它們是直接定義在原型上的函數,不知道怎麼,就把它們給忽略了。html

jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " +
  "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
  "change select submit keydown keypress keyup contextmenu" ).split( " " ),
  function( i, name ) {

  // Handle event binding
  jQuery.fn[ name ] = function( data, fn ) {
    return arguments.length > 0 ?
      this.on( name, null, data, fn ) :
      this.trigger( name );
  };
} );

這個構造也是十分巧妙的,這些方法組成的字符串經過 split(" ") 變成數組,然後又經過 each 方法,在原型上對應每一個名稱,定義函數,這裏能夠看到,依舊是 on,還有 targger:java

jQuery.fn.extend( {
  trigger: function(type, data){
    return this.each(function (){
      // // 依舊是 event 對象上的方法
      jQuery.event.trigger(type, data, this);
    })
  }
} )

還缺乏一個 one 方法,這個方法表示綁定的事件同類型只執行一次,.one()git

jQuery.fn.extend( {
  one: function( types, selector, data, fn ) {
    // 全局 on 函數
    return on( this, types, selector, data, fn, 1 );
  },
} );

DOM 事件知識點

發現隨着 event 源碼的不斷的深刻,我本身出現愈來愈多的問題,好比沒有看到我所熟悉的 addEventListener,還有一些看得很迷糊的 events 事件,因此我決定仍是先來看懂 JS 中的 DOM 事件吧。github

早期 DOM 事件

在 HTML 的 DOM 對象中,有一些以 on 開頭的熟悉,好比 onclick、onmouseout 等,這些就是早期的 DOM 事件,它的最簡單的用法,就是支持直接在對象上以名稱來寫函數:web

document.getElementsByTagName('body')[0].onclick = function(){
  console.log('click!');
}
document.getElementsByTagName('body')[0].onmouseout = function(){
  console.log('mouse out!');
}

onclick 函數會默認傳入一個 event 參數,表示觸發事件時的狀態,包括觸發對象,座標等等。segmentfault

這種方式有一個很是大的弊端,就是相同名稱的事件,會先後覆蓋,後一個 click 函數會把前一個 click 函數覆蓋掉:api

var body = document.getElementsByTagName('body')[0];
body.onclick = function(){
  console.log('click1');
}
body.onclick = function(){
  console.log('click2');
}
// "click2"
body.onclick = null;
// 沒有效果

DOM 2.0

隨着 DOM 的發展,已經來到 2.0 時代,也就是我所熟悉的 addEventListener 和 attachEvent(IE),JS 中的事件冒泡與捕獲。這個時候和以前相比,變化真的是太大了,MDN addEventListener()數組

變化雖然是變化了,可是瀏覽器的兼容卻成了一個大問題,好比下面就能夠實現不支持 addEventListener 瀏覽器:

(function() {
  // 不支持 preventDefault
  if (!Event.prototype.preventDefault) {
    Event.prototype.preventDefault=function() {
      this.returnValue=false;
    };
  }
  // 不支持 stopPropagation
  if (!Event.prototype.stopPropagation) {
    Event.prototype.stopPropagation=function() {
      this.cancelBubble=true;
    };
  }
  // 不支持 addEventListener 時候
  if (!Element.prototype.addEventListener) {
    var eventListeners=[];
    
    var addEventListener=function(type,listener /*, useCapture (will be ignored) */) {
      var self=this;
      var wrapper=function(e) {
        e.target=e.srcElement;
        e.currentTarget=self;
        if (typeof listener.handleEvent != 'undefined') {
          listener.handleEvent(e);
        } else {
          listener.call(self,e);
        }
      };
      if (type=="DOMContentLoaded") {
        var wrapper2=function(e) {
          if (document.readyState=="complete") {
            wrapper(e);
          }
        };
        document.attachEvent("onreadystatechange",wrapper2);
        eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper2});
        
        if (document.readyState=="complete") {
          var e=new Event();
          e.srcElement=window;
          wrapper2(e);
        }
      } else {
        this.attachEvent("on"+type,wrapper);
        eventListeners.push({object:this,type:type,listener:listener,wrapper:wrapper});
      }
    };
    var removeEventListener=function(type,listener /*, useCapture (will be ignored) */) {
      var counter=0;
      while (counter<eventListeners.length) {
        var eventListener=eventListeners[counter];
        if (eventListener.object==this && eventListener.type==type && eventListener.listener==listener) {
          if (type=="DOMContentLoaded") {
            this.detachEvent("onreadystatechange",eventListener.wrapper);
          } else {
            this.detachEvent("on"+type,eventListener.wrapper);
          }
          eventListeners.splice(counter, 1);
          break;
        }
        ++counter;
      }
    };
    Element.prototype.addEventListener=addEventListener;
    Element.prototype.removeEventListener=removeEventListener;
    if (HTMLDocument) {
      HTMLDocument.prototype.addEventListener=addEventListener;
      HTMLDocument.prototype.removeEventListener=removeEventListener;
    }
    if (Window) {
      Window.prototype.addEventListener=addEventListener;
      Window.prototype.removeEventListener=removeEventListener;
    }
  }
})();

雖然不支持 addEventListener 的瀏覽器能夠實現這個功能,但本質上仍是經過 attachEvent 函數來實現的,在理解 DOM 早期的事件如何來創建仍是比較捉急的。

addEvent 庫

addEvent庫的這篇博客發表於 2005 年 10 月,因此這篇博客所講述的 addEvent 方法算是經典型的,就連 jQuery 中的事件方法也是借鑑於此,故值得一提:

function addEvent(element, type, handler) {
  // 給每個要綁定的函數添加一個標識 guid
  if (!handler.$$guid) handler.$$guid = addEvent.guid++;
  // 在綁定的對象事件上建立一個事件對象
  if (!element.events) element.events = {};
  // 一個 type 對應一個 handlers 對象,好比 click 可同時處理多個函數
  var handlers = element.events[type];
  if (!handlers) {
    handlers = element.events[type] = {};
    // 若是 onclick 已經存在一個函數,拿過來
    if (element["on" + type]) {
      handlers[0] = element["on" + type];
    }
  }
  // 防止重複綁定,每一個對應一個 guid
  handlers[handler.$$guid] = handler;
  // 把 onclick 函數替換成 handleEvent
  element["on" + type] = handleEvent;
};
// 初始 guid
addEvent.guid = 1;

function removeEvent(element, type, handler) {
  // delete the event handler from the hash table
  if (element.events && element.events[type]) {
    delete element.events[type][handler.$$guid];
  }
  // 感受後面是否是要加個判斷,當 element.events[type] 爲空時,一塊兒刪了
};

function handleEvent(event) {
  // grab the event object (IE uses a global event object)
  event = event || window.event;
  // 這裏的 this 指向 element
  var handlers = this.events[event.type];
  // execute each event handler
  for (var i in handlers) {
    // 這裏有個小技巧,爲何不直接執行,而是先綁定到 this 後執行
    // 是爲了讓函數執行的時候,內部 this 指向 element
    this.$$handleEvent = handlers[i];
    this.$$handleEvent(event);
  }
};

若是能將上面 addEvent 庫的這些代碼看懂,那麼在看 jQuery 的 events 源碼就明朗多了。

還有一個問題,所謂事件監聽,是將事件綁定到父元素或 document 上,子元素來響應,如何實現?

要靠 event 傳入的參數 e:

var body = document.getElementsByTagName('body')[0];
body.onclick = function(e){
  console.log(e.target.className);
}

這個 e.target 對象就是點擊的那個子元素了,不管是捕獲也好,冒泡也好,貌似都可以模擬出來。接下來,可能要真的步入正題了。

總結

感受事件委託的代碼仍是至關複雜的,我本身也啃了好多天,有那麼一點點頭緒,其中還有不少模模糊糊的知識點,只是以爲,存在就是牛逼的,我看不懂,但不表明它不牛逼。

參考

jQuery 2.0.3 源碼分析 事件體系結構
addEvent庫
MDN EventTarget.addEventListener()
原生JavaScript事件詳解

本文在 github 上的源碼地址,歡迎來 star。

歡迎來個人博客交流。

相關文章
相關標籤/搜索