jQuery 源碼系列(十四)自定義事件

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

之前,我只知道,只有當對瀏覽器中的元素進行點擊的時候,纔會出發 click 事件,其它的事件也同樣,須要人爲的鼠標操做。css

後來隨着學習的不斷深刻,才知道原來 JS 能夠寫函數來控制事件的執行,這樣子寫代碼纔有意思。記得好久好久之前一些惡意網站,明明鼠標沒有點擊,卻被網站強行的點擊了某個連接,大概實現的方式就是這樣的吧。html

原生事件

其實 JS 的原生事件已經作得挺好了,只是 jQuery 將其進行封裝,作的更好。java

關於 document.createEvent,下面是一個簡單的事件點擊的例子:node

var fn = function(){
  console.log('click');
}

var button = document.getElementById('#id');
button.addEventListener('click', fn);

// 點擊事件 MouseEvent
var myClick = document.createEvent('MouseEvent');
myClick.initMouseEvent('click', false, false, null);

// 執行
button.dispatchEvent(myClick); // 'click'

除了鼠標事件,還能夠自定義事件:git

// 隨便自定義一個事件 test.click
button.addEventListener('test.click', fn);

var testEvent = document.createEvent('CustomEvent');
// customEvent 也能夠初始化爲鼠標事件,不必定非要自定義事件
testEvent.initCustomEvent('test.click', false, false, null);

button.dispatchEvent(testEvent); // 'click'

JS 原生的模擬事件,使用起來仍是很方便的,以上即是原生的。github

不過 jQuery 也有本身的一套自定義事件方案。segmentfault

jQuery.trigger

jQuery.trigger 能夠和 HTMLElement.dispatchEvent 事件拿來對比,他們都是用來模擬和執行監聽的事件。api

如何使用

關於使用,則比較簡單了.trigger():數組

var $body = $(document.body);

// 先綁定事件
$body.on('click', function(){
  console.log('click');
})

// 執行
$body.trigger('click'); //'click'

trigger 還支持更多的參數,一樣能夠自定義事件:

$body.on('click.test', function(e, data1, data2){
  console.log(data1 + '-' + data2);
})

$body.trigger('click.test', ['hello', 'world']);

trigger 源碼

trigger 的源碼有些簡單,由於仍是要藉助於 jQuery.event 來處理:

jQuery.fn.extend( {
  trigger: function(type, data){
    return this.each(function(){
      jQuery.event.trigger(type, data, this);
    })
  },
  // triggerHandler 處理第一個且不觸發默認事件
  triggerHandler: function( type, data ) {
    var elem = this[ 0 ];
    if ( elem ) {
      return jQuery.event.trigger( type, data, elem, true );
    }
  }
} );

因此 trigger 事件的起點又回到了 jQuery.event

jQuery.event.trigger

其實 trigger 和 add + handler 函數很相似,大體都是從 data cache 中搜索緩存,執行回調函數。須要考慮要不要執行默認事件,即第四個參數爲 true 的狀況。

jQuery.extend(jQuery.event, {
  // onleyHandlers 表示不考慮冒泡事件
  trigger: function( event, data, elem, onlyHandlers ) {

    var i, cur, tmp, bubbleType, ontype, handle, special,
      eventPath = [ elem || document ],
      type = hasOwn.call( event, "type" ) ? event.type : event,
      namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : [];

    cur = tmp = elem = elem || document;

    // Don't do events on text and comment nodes
    if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
      return;
    }

    // 異步不衝突
    if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
      return;
    }

    if ( type.indexOf( "." ) > -1 ) {

      // Namespaced trigger; create a regexp to match event type in handle()
      namespaces = type.split( "." );
      type = namespaces.shift();
      namespaces.sort();
    }
    ontype = type.indexOf( ":" ) < 0 && "on" + type;

    // 改裝原生的 event 事件
    event = event[ jQuery.expando ] ?
      event :
      new jQuery.Event( type, typeof event === "object" && event );

    // 判斷是否只執行當前 trigger 事件,不冒泡
    event.isTrigger = onlyHandlers ? 2 : 3;
    event.namespace = namespaces.join( "." );
    event.rnamespace = event.namespace ?
      new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) :
      null;

    // Clean up the event in case it is being reused
    event.result = undefined;
    if ( !event.target ) {
      event.target = elem;
    }

    // Clone any incoming data and prepend the event, creating the handler arg list
    data = data == null ?
      [ event ] :
      jQuery.makeArray( data, [ event ] );

    // Allow special events to draw outside the lines
    special = jQuery.event.special[ type ] || {};
    if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
      return;
    }

    // 向 document 冒泡並把冒泡結果存儲到 eventPath 數組中
    if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {

      bubbleType = special.delegateType || type;
      if ( !rfocusMorph.test( bubbleType + type ) ) {
        cur = cur.parentNode;
      }
      for ( ; cur; cur = cur.parentNode ) {
        eventPath.push( cur );
        tmp = cur;
      }

      // Only add window if we got to document (e.g., not plain obj or detached DOM)
      if ( tmp === ( elem.ownerDocument || document ) ) {
        eventPath.push( tmp.defaultView || tmp.parentWindow || window );
      }
    }

    // 按需求來執行
    i = 0;
    while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) {

      event.type = i > 1 ?
        bubbleType :
        special.bindType || type;

      // 從 data cache 中得到回調函數
      handle = ( dataPriv.get( cur, "events" ) || {} )[ event.type ] &&
        dataPriv.get( cur, "handle" );
      if ( handle ) {
        // 執行
        handle.apply( cur, data );
      }

      // Native handler
      handle = ontype && cur[ ontype ];
      if ( handle && handle.apply && acceptData( cur ) ) {
        event.result = handle.apply( cur, data );
        if ( event.result === false ) {
          event.preventDefault();
        }
      }
    }
    event.type = type;

    // If nobody prevented the default action, do it now
    if ( !onlyHandlers && !event.isDefaultPrevented() ) {

      if ( ( !special._default ||
        special._default.apply( eventPath.pop(), data ) === false ) &&
        acceptData( elem ) ) {

        // Call a native DOM method on the target with the same name as the event.
        // Don't do default actions on window, that's where global variables be (#6170)
        if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {

          // Don't re-trigger an onFOO event when we call its FOO() method
          tmp = elem[ ontype ];

          if ( tmp ) {
            elem[ ontype ] = null;
          }

          // Prevent re-triggering of the same event, since we already bubbled it above
          jQuery.event.triggered = type;
          elem[ type ]();
          jQuery.event.triggered = undefined;

          if ( tmp ) {
            elem[ ontype ] = tmp;
          }
        }
      }
    }

    return event.result;
  },
})

總結

jQuery.event.trigger 中,比較有意思的是模擬冒泡機制,大體的思路就是:

  1. 把當前 elem 存入數組;

  2. 查找當前 elem 的父元素,若是符合,push 到數組中,重複第一步,不然下一步;

  3. 遍歷數組,從 data cache 中查看是否綁定 type 事件,而後依次執行。

冒泡事件就是就是由內向外冒泡的過程,這個過程不是很複雜。

event 事件應該就這麼多內容吧。

參考

解密jQuery事件核心 - 自定義設計(三)
MDN createEvent
解密jQuery事件核心 - 模擬事件(四)

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

歡迎來個人博客交流。

相關文章
相關標籤/搜索