一個普通的 Zepto 源碼分析(三) - event 模塊


一個普通的 Zepto 源碼分析(三) - event 模塊

普通的路人,普通地瞧。分析時使用的是目前最新 1.2.0 版本。

Zepto 可以由許多模塊組成,默認包含的模塊有 zepto 核心模塊,以及 event 、 ajax 、 form 、 ie ,其中 event 模塊也是比較重要的模塊之一,我們可以借助它提供的方法實現事件的監聽、自定義事件的派發等。最重要的是,做了一些事件的兼容,簡化了我們的編碼。

event 模塊

這個模塊代碼行數要比 ajax 的少。還是老套路,對函數調用關系做個靜態分析,結果得到一坨群魔亂舞的線條(...)。

好在有一些函數可以直接略過。

  $.fn.bind = function(event, data, callback){
    return this.on(event, data, callback)
  }
  $.fn.unbind = function(event, callback){
    return this.off(event, callback)
  }
  $.fn.one = function(event, selector, data, callback){
    return this.on(event, selector, data, callback, 1)
  }
  $.fn.delegate = function(selector, event, callback){
    return this.on(event, selector, callback)
  }
  $.fn.undelegate = function(selector, event, callback){
    return this.off(event, selector, callback)
  }

  $.fn.live = function(event, callback){
    $(document.body).delegate(this.selector, event, callback)
    return this
  }
  $.fn.die = function(event, callback){
    $(document.body).undelegate(this.selector, event, callback)
    return this
  }

除了 $.fn.one() 外,其余函數都已經廢棄,可用 $.fn.on()$.fn.off() 代替。但是對 $.fn.on() 統一后會不會顯得功能很重呢?見仁見智,原本我認為會,但其實靈活的參數列表帶來開發效率的上升,並且少記很多相似的函數。

發布 / 訂閱模式

簡化 $.fn.on()$.fn.off() 后我們可以看到,它們的最終歸宿分別是 add()remove() 閉包函數:

  // 注冊回調
  $.fn.on = function(event, selector, data, callback, one){
    var autoRemove, delegator, $this = this
    if (event && !isString(event)) {
      // 枚舉 事件類型-回調 對象
      $.each(event, function(type, fn){
        $this.on(type, selector, data, fn, one)
      })
      // 返回 this 保證鏈式調用
      return $this
    }

    ... // 傳入參數的重載等處理

    return $this.each(function(_, element){
      if (one) autoRemove = function(e){...}

      if (selector) delegator = function(e){...}

      add(element, event, callback, data, selector, delegator || autoRemove)
    })
  }
  // 解綁回調
  $.fn.off = function(event, selector, callback){
    var $this = this
    if (event && !isString(event)) {
      $.each(event, function(type, fn){
        $this.off(type, selector, fn)
      })
      return $this
    }

    ... // 傳入參數的重載等處理

    return $this.each(function(){
      remove(this, event, callback, selector)
    })
  }

這里的 event 可以是一個由空格分割的事件類型字符串,也可以是一個可枚舉的事件類型/回調 k-v 對象。所以一開頭會在 $.each() 中對每個 k-v 調用自身。

先不管 add()remove() 內部是怎樣的邏輯(如對多回調的處理、暫存等),可以看到最終都調用了瀏覽器方法:

  function add(element, events, fn, data, selector, delegator, capture){
    ...
      if ('addEventListener' in element)
        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    ...
  }
  function remove(element, events, fn, selector, capture){
    ...
      if ('removeEventListener' in element)
        element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    ...
  }

利用了瀏覽器 BOM 的事件監聽方法,維護了多個回調函數對事件的訂閱。

而瀏覽器本身會產生事件(如用戶操作時),它就是一個發布者。另外我們也有可自定義的發布者接口 $.fn.trigger()$.fn.triggerHandler() 用於派發自定義類型的事件,前者可通過 BOM 接口 dispatchEvent() 派發事件,后者通過內部的代理閉包來一次性地觸發事件回調。

有人認為 Event 模塊本身類似於代理者,但我認為不太恰當,它只簡單提供了注冊、解注冊、下發事件的接口,沒有明確的控制機制,即使是(后面提到的)事件命名空間也可看作不同的事件。實際上,無論是 jQuery 還是 Zepto 都是將 window 對象當作 event bus 的,每個 listener 只管訂閱自己范圍內的消息,事件的調度(派發、冒泡)是通過 DOM 事件流完成的(見 w3.org 的事件流圖 ),這當然是實打實的發布/訂閱模式。

基於 event bus 的事件機制

上面我們說到是將 window 對象作為 event bus 來實現事件機制的。由於存在事件派發與冒泡機制,事件傳播的路徑形成了 DOM 事件流,而對於特定的事件目標,這條傳播路徑是唯一確定的,因為 DOM 樹內任何一個非根節點有且只有唯一的一個父節點。而從 w3.org 的事件流圖 我們可以看到,一旦傳播路徑確定了,事件過程可分為 3 個階段:捕獲階段、目標階段、冒泡階段。位於目標元素之上的祖先可以選擇捕獲和冒泡,也可以取消掉后續整個傳播。當然啦,在目標階段也可以取消掉目標元素的默認行為。

盡管事件的形成、傳播、觸發都是由瀏覽器完成的,但 addEventListener() 卻是注冊到事件目標上的。我們可以將事件流想象一條虛擬的支線:

Window 總線 -- targetA.click -- targetB.Event0 -- ... -->>
                      |
為 targetA 創造的事件流 -- Document -- ... -- parent -- targetA -- parent -- ... -->>
                            ^                          ^  ^        ^
                            |                          |  |        |
                        Listener 捕獲                 可能多個   Listener 冒泡
                                                     Listener

當然啦,盡管每次邏輯上通過的事件流可能不一樣,但實際的 Listener 還是掛在 DOM 樹節點上不會變的。

compatible() 兼容修正函數

在開始具體分析之前,必須先搞清楚幾個入度較高的函數。首當其沖的應該是 compatible() 函數了,它被 Event 模塊內部的方法調用有 4 處,是模塊內最高的了。由靜態分析可知,大體有兩種調用情況:

  // 假設 proxyEvent 是代理事件, nativeEvent 是原生事件
  compatible(nativeEvent)
  compatible(proxyEvent, nativeEvent)

而創造事件代理的函數是這樣的:

  var ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$|webkitMovement[XY]$)/
  function createProxy(event) {
    var key, proxy = { originalEvent: event }
    for (key in event)
      if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]

    return compatible(proxy, event)
  }

event[key] !== undefined 是 for...in 的常見操作,可以保證只繼承具有有效值的屬性。

其中這個 ignoreProperties 很奇怪,我翻到了 v1.0 的一個提交 18ba1f0 (增加了 [A-Z]|layer[XY]$ ),它是這么說的:

silence "layerX/Y" Webkit warnings for events

This means we can't just blindly extend all event properties onto the
event proxy object.

v1.1.0 的一個提交 b0eaeb7 (增加了 returnValue$ )則說 Chrome 廢棄了該方法,即使是復制也會發出警告。

v1.1.7 的一個提交 c89e705 (增加了 webkitMovement[XY]$ )也說是為了消除 Chrome 的警告。

看來兼容性問題可以治愈強迫症(逃。至於瀏覽器做廢棄檢查和代碼中先行檢查哪個性能損耗代價大,我沒比較過,直覺是前者大點,畢竟還要准備發警告的工作。

由上可以得知事件代理其實是一個繼承了原來的事件大部分屬性的對象,並會在內部維護一個對原事件對象的引用。好的,現在來看看 compatible() 的具體實現:

  var returnTrue = function(){return true},
      returnFalse = function(){return false},
      /* 關注點 1 (被代理函數名對應斷言表) */
      eventMethods = {
        preventDefault: 'isDefaultPrevented',
        stopImmediatePropagation: 'isImmediatePropagationStopped',
        stopPropagation: 'isPropagationStopped'
      }
  function compatible(event, source) {
    if (source || !event.isDefaultPrevented) {
      source || (source = event)
      // 關注點 1 (被代理函數名對應斷言表)
      $.each(eventMethods, function(name, predicate) {
        var sourceMethod = source[name]
        event[name] = function(){
          // 關注點 2 (設置條件樁函數)
          this[predicate] = returnTrue
          return sourceMethod && sourceMethod.apply(source, arguments)
        }
        event[predicate] = returnFalse
      })

      try {
        event.timeStamp || (event.timeStamp = Date.now())
      } catch (ignored) { }
      // 關注點 3 (為支持跨平台,順序:新瀏覽器、老式方法、非常早期的廢棄 API )
      if (source.defaultPrevented !== undefined ? source.defaultPrevented :
          'returnValue' in source ? source.returnValue === false :
          source.getPreventDefault && source.getPreventDefault())
        event.isDefaultPrevented = returnTrue
    }
    return event
  }

首先要保證是代理調用,或者 isDefaultPrevented 屬性沒有被設置過,否則無需處理直接返回。也就是說,很多人認為的它會多重打包其實是不存在的,只有可能是代理事件對象再次被代理。下面可以看到這個樁函數必定會被設置。

接着是對 3 個原生函數的一個代理封裝,使得每次調用都會對相應的斷言(作為函數名)設置一次條件樁函數,再調用回原來的函數。而默認的樁函數總是返回 false 代表對應方法還未被調用過。最后如果事件的默認動作已被取消,則相應條件樁應一直返回 true 。

另根據 4f3d4a8 在 safari 上 event.timeStamp 可能是只讀的,只能忽略對時間戳的設置。

綜上, compatible() 是一個兼容修正器,用來裝飾上 Event 插件要提供的 3 個條件樁函數。

$.Event() 生成自定義事件

它允許我們指定自定義的事件類型,創造一個事件對象,最后可以將它觸發(如 trigger() 等)。

  var specialEvents={}
  specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'

  $.Event = function(type, props) {
    // 參數重載
    if (!isString(type)) props = type, type = props.type
    var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true
    if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
    event.initEvent(type, bubbles, true)
    return compatible(event)
  }

現在鼓勵使用事件構造函數來 new 一個事件,如 new Event('xxx')new CustomEvent('xxx', {...})new MouseEvent('click', {...}) 等。但是早期的瀏覽器只支持 createEvent() 方法來創造事件,參數可以為 UIEventsMouseEventsMutationEventsHTMLEvents 以及其他非標准事件等(如 Gecko 自己定義的事件類型)。有點誇張的是, initEvent() 已經被廢棄了= = 唔,總之這里就是一些黑科技啦。

最后返回一個經過兼容修正的事件對象。

$.proxy 函數代理

該函數給傳入的上下文環境或函數提供一層簡單的代理,使得傳入函數在調用的時候其 this 指針指向傳入的上下文對象,其實這有點像 ES6 的 bind() 函數了;或者有第二種形式,事先將函數賦給傳入的上下文對象的一個屬性,並傳入該屬性名。

  $.proxy = function(fn, context) {
    var args = (2 in arguments) && slice.call(arguments, 2)
    if (isFunction(fn)) {
      var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
      // 關注點 1 (代理函數與原函數被視作同一回調)
      proxyFn._zid = zid(fn)
      return proxyFn
    } else if (isString(context)) {
      if (args) {
        // 關注點 2 (簡單重載)
        args.unshift(fn[context], fn)
        return $.proxy.apply(null, args)
      } else {
        return $.proxy(fn[context], fn)
      }
    } else {
      throw new TypeError("expected function")
    }
  }

首先 proxyFn 是一個閉包函數。但是 proxyFn._zid = zid(fn) 這個操作有點奇怪。查找 zid() 的引用發現其他地方還有一個 zid(handler.fn) === zid(fn) 的判斷。看來當作為事件回調函數時,會被認為同一個函數,也就是說在 $.fn.off() 的時候只要傳入原函數,即會解除代理函數的事件回調。即使是把原函數再代理一遍(比如多次更換上下文對象等),也會一並被找到並解除。

對於第二種調用形式,比較簡單粗暴,算是一種快捷重載吧。

深入 add() 函數

綁定回調句柄及其暫存集合

先看兩個直接調用的函數。一開始用於處理參數的函數,它們或許會很重要:

  var _zid = 1,
      handlers = {}
  function zid(element) {
    return element._zid || (element._zid = _zid++)
  }
  function parse(event) {
    var parts = ('' + event).split('.')
    return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
  }
  function add(element, events, fn, data, selector, delegator, capture){
    // 關注點 1 (綁定回調句柄及其集合)
    var id = zid(element), set = (handlers[id] || (handlers[id] = []))
    events.split(/\s/).forEach(function(event){
      if (event == 'ready') return $(document).ready(fn)
      var handler   = parse(event)
      ...
      // 關注點 2 (記錄進集合)
      handler.i = set.length
      set.push(handler)
      ...
    })
  }

這里為原生對象綁定了一個自增的 _zid ,而不是綁定到 Zepto 對象上。因為每次 $() 拿到的封裝對象都是新 new 出來的。另外 DOM 本身就是個巨大的多級表,不需要再對 DOM 元素額外維護一個映射表,直接給 DOM 元素添加 id 屬性就好了,反過來我們還可以利用這個 id 作為其元素的索引。

綁定了 id 就要用,接下來看到 set = (handlers[id] || (handlers[id] = [])) 維護了一個事件句柄 handler 對象的暫存集合。后面利用自增的數組長度作為新加入對象的序號標記。

parse() 函數用於解析單個事件的命名空間,在 Zepto 的文檔上沒有提到,翻 jQuery 的 event.namespace 才找到說明,主要是根據命名空間,為同一事件響應屬於不同命名空間子集的回調函數。這時事件類型大概會長成這個樣子(是由用戶傳入的):

    test.somethingA
    ErrorEvent.otherthingB.orPluginC.orSubscriberD

模擬 mouseentermouseleave 事件

很多人認為模擬這兩個事件是為了支持往祖先冒泡, emmm.. 或許吧,但我是支持不冒泡的,除了統一事件委托到父元素,天知道為什么會有父元素要在冒泡階段知悉子元素被進入/離開的需求。我認為更多還是兼容性的問題,早期瀏覽器是並不支持這兩個事件的。

  var hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }
  function add(element, events, fn, data, selector, delegator, capture){
    ...
      var handler   = parse(event)
      handler.fn    = fn
      handler.sel   = selector
      // emulate mouseenter, mouseleave
      if (handler.e in hover) fn = function(e){
        // 關注點 1 (該屬性的使用)
        var related = e.relatedTarget
        // 關注點 2 (包含性查找)
        if (!related || (related !== this && !$.contains(this, related)))
          return handler.fn.apply(this, arguments)
      }
    ...
  }

handler.sel 會在查找 handler 的時候用到,這里先不管。

如何模擬?顯然如果我們能知道在鼠標移動的時候,指針指向了哪個元素就好了,最多也就監控一下這個指向是否發生了變化而已。

萬幸的是,我們有 .relatedTarget 只讀屬性。根據 MDN 上 MouseEvent.relatedTarget 的介紹, mouseover 事件的 relatedTarget 會指向“從哪里來”的元素,而 mouseout 事件的則會指向“到哪里去”的元素。以 mouseover 事件為例,我們可能從外部進入,也可能從子元素(移出)進入,從子元素移入的事件會被冒泡上來,我們可以很好地用 $.contains() 判斷這個子元素是在我們的事件目標元素之下的。

要注意的是 target 屬性正好反過來,還是以 mouseover 事件為例,不管是從外部進入觸發的,還是子元素冒泡上來的,其 target 屬性永遠都是指向我們的事件目標元素,無法將二者區分開來。

事件委托與代理

我們需要看回原本 $.fn.on()delegator 參數中傳了個什么樣的委托進來。

  $.fn.on = function(event, selector, data, callback, one){
    ...
    return $this.each(function(_, element){
      // 關注點 1 (自動解綁的委托)
      if (one) autoRemove = function(e){
        remove(element, e.type, callback)
        return callback.apply(this, arguments)
      }
      // 關注點 1 (匹配選擇符的委托)
      if (selector) delegator = function(e){
        // 關注點 2 (向上查找最近節點)
        var evt, match = $(e.target).closest(selector, element).get(0)
        if (match && match !== element) {
          evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
          // 關注點 3
          return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
        }
      }

      add(element, event, callback, data, selector, delegator || autoRemove)
    })
  }

autoRemove 很好理解,它是一個自動解綁的委托:當被調用的時候先把對應的一次性回調函數移除,然后執行它的歷史使命。為什么要先移除?較大可能是因為回調函數是用戶自定義的,如果出現未捕獲的異常會中斷代碼執行,不能正常移除。

delegator 則是一個條件委托,只有當事件源元素符合給定的 CSS 選擇器時,事件才能夠被響應。當然由於冒泡的存在,冒泡路徑上的元素都是事件源元素,所以每次都會從事件源開始往上查找匹配 CSS 選擇器的第一個元素,直到超出給定的 element 范圍為止(即不是它的后代節點)。

接下來則會為事件創建代理,並添加兩個屬性,分別是符合目標的元素,以及激發事件的元素。然后是完成委托任務,如果 autoRemove 委托存在則交由它來執行,否則自行調用回調函數。最后傳入 add() 函數做進一步的處理~

現在再看回 add() 函數中的事件代理:

      handler.del   = delegator
      var callback  = delegator || fn
      handler.proxy = function(e){
        e = compatible(e)
        // 關注點 1
        if (e.isImmediatePropagationStopped()) return
        // 關注點 2
        e.data = data
        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
        // 關注點 3
        if (result === false) e.preventDefault(), e.stopPropagation()
        return result
      }

由於 stopImmediatePropagation() 的效果不但是阻斷傳播路徑、阻止事件冒泡,還要阻止后面其他回調的響應,因此需要在代理中判斷該函數有沒有執行過,當沒有被阻止執行,才會執行回調函數。而回調函數的執行結果也會影響后續冒泡。

注冊監聽

這里就比較簡單了,只有兩個微小的操作需要注意。

  // 關注點 2 (對 focusin / focusout 的漸進增強轉換)
  var focusinSupported = 'onfocusin' in window,
      focus = { focus: 'focusin', blur: 'focusout' },
      hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }
  // 關注點 3 (偽兼容支持..)
  function eventCapture(handler, captureSetting) {
    return handler.del &&
      (!focusinSupported && (handler.e in focus)) ||
      !!captureSetting
  }
  // 關注點 2
  function realEvent(type) {
    return hover[type] || (focusinSupported && focus[type]) || type
  }
  function add(element, events, fn, data, selector, delegator, capture){
    ...
    events.split(/\s/).forEach(function(event){
      ...
      var handler   = parse(event)
      ...
      if (handler.e in hover) fn = function(e){...}
      ...
      handler.proxy = function(e){...}
      ...
      // 關注點 1
      if ('addEventListener' in element)
        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
  }

首先是一個兼容性的處理,對於 mouseentermouseout 的模擬前面已經探究過了,這里還有個問題是還有相當多的瀏覽器竟然不支持 focusin / focusout 事件,比如(主要是)直至今年一月份的 FF 。因此我們只能使用較老的事件類型名 focus / blur 。而 realEvent() 就是在做事件類型名的切換工作。

此外,前者是在元素獲得或失去焦點產生的,會冒泡;后者則是在焦點轉移后才觸發,且不會冒泡。對於這一兼容問題,沒有太好的模擬方案,只能在捕獲階段就觸發,造成一種冒泡的假象。實際上觸發的順序還是向下傳播的順序,而 stopPropagation() 會斷掉整個傳播路徑,所以使用時要小心。這一操作體現在 eventCapture() 內。

最后有一個 captureSetting 的參數,找不到任何從用戶傳入的途徑,可能是 Zepto 的 Event 模塊並沒有提供注冊捕獲階段回調的接口。

深入 remove 函數

查找回調句柄對象

首先要看 $.fn.off() 函數的說明:

  1. 要傳入與調用 $.fn.on() 時相同的函數;
  2. 如果只傳入事件類型名,會解綁所有該類型的事件回調;
  3. 如果什么都不傳,解綁當前元素的所有事件回調。

第一點或許有疑問,怎么知道是不是相同的函數呢?我們在上面知道每個元素會綁定一個 _zid ,該模塊以每個 id 為索引維護了一個關於 handler 的集合,而我們傳入的回調函數綁定在 handler.fn 上。看來可以想辦法找到這些 handler

  function zid(element) {
    return element._zid || (element._zid = _zid++)
  }
  function findHandlers(element, event, fn, selector) {
    event = parse(event)
    // 關注點 1 (生成匹配命名空間的正則)
    if (event.ns) var matcher = matcherFor(event.ns)
    return (handlers[zid(element)] || []).filter(function(handler) {
      // 關注點 3 (篩選符合條件的 handler )
      return handler
        && (!event.e  || handler.e == event.e)
        && (!event.ns || matcher.test(handler.ns))
        && (!fn       || zid(handler.fn) === zid(fn))
        && (!selector || handler.sel == selector)
    })
  }
  function parse(event) {
    var parts = ('' + event).split('.')
    // 關注點 2 (命名空間字典序)
    return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
  }
  function matcherFor(ns) {
    // 關注點 1
    return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')
  }

由於有事件命名空間的存在,查找過程需要多費點勁。看起來 matcherFor() 就是干這個的。

我們知道擁有多個命名空間的事件可能長這樣: click.nsF.nsA.nsC ,而最終 event.ns 則應該是 nsA nsC nsF (按字典序)。如果我想匹配它怎么辦呢?不好直接等於,因為像 nsA nsB nsC nsF 這樣的跟它是包含關系。我們需要一個正則表達式把插在兩邊、插在中間的命名空間過濾掉。

過濾兩邊是很好辦的,因為命名空間字符串是以空格分割的,這也是兩個捕獲組 (?:^| )(?: |$) 的由來。

過濾中間也比較好辦,用一個非貪婪匹配掉任意字符就好了,也就是 .*? 的作用。

於是像 (?:^| )nsA .* ?nsC .* ?nsF(?: |$) 這樣的正則可以匹配出下面這些:

  nsA nsC nsF
  nsA nsB nsC nsF
  ns0 nsA nsB nsC nsF
  ns9 nsA nsB nsC nsD nsE nsF nsZ

不過很明顯.. Zepto 又寫錯了.replace(' ', ' .* ?') 只會匹配替換第一次出現的空格,不滿足需求。正確的替換應該是 .replace(/ /g, ' .* ?')

之后的過程就比較簡單啦,拿到 handlers[_zid] 數組,把每個綁定的 handler 句柄對象篩一次就好了。由於四個條件均是可選的,用了一個 !xxx || yyy 的形式,保證只有存在才執行后面的相等判斷,不存在則跳過。

remove() 函數具體實現

接下來就很簡單了。

  function remove(element, events, fn, selector, capture){
    var id = zid(element)
    ;(events || '').split(/\s/).forEach(function(event){
      // 關注點 1 (找出符合條件的 handler 數組)
      findHandlers(element, event, fn, selector).forEach(function(handler){
        // 關注點 2 (刪除數組元素)
        delete handlers[id][handler.i]
      if ('removeEventListener' in element)
        element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
      })
    })
  }

找到了對應的 handler 之后(可能不止一個哦),直接粗暴的 delete 掉.. 性能是保證了,但是空間浪費了,仍然會有一個 undefined 的“坑”留在那里,也並不會被后續綁定的 handler 填上。不過鑒於一個元素也綁定不了多少事件回調函數,也就湊合用了。

較早之前我原以為會是維護隊列或者鏈式調用的,沒想到竟然..

useCapture 分別為 true 或 false 的 listener 會被認為是兩個不同的,因此移除事件監聽也要把這個參數考慮進去。

事件的觸發

首先來看 $.fn.triggerHandler() ,它只觸發與事件相關的回調,並不會真正地把事件派發。

  // triggers event handlers on current element just as if an event occurred,
  // doesn't trigger an actual event, doesn't bubble
  $.fn.triggerHandler = function(event, args){
    var e, result
    this.each(function(i, element){
      // 關注點 1 (包裹一層事件代理)
      e = createProxy(isString(event) ? $.Event(event) : event)
      e._args = args
      e.target = element
      // 關注點 2 (直接觸發所有關聯的句柄對象)
      $.each(findHandlers(element, event.type || event), function(i, handler){
        result = handler.proxy(e)
        if (e.isImmediatePropagationStopped()) return false
      })
    })
    return result
  }

這里首先會做一個事件代理,避免直接修改原生的事件對象。在找到對應事件類型的 handler 后由其代理函數 handler.proxy() 執行響應。由於不經過 DOM 事件流,這種直接觸發自然就不會冒泡。

在有多個 handler 的情況下如果被 stopImmediatePropagation() 了,則會終止遍歷,不再觸發后續 handler (觸發的順序應該遵循先后)。該函數的返回值取決於最后一個 handler 響應事件的返回值。

另外還記得 handler.proxy() 中有一句:

        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))

只有手動觸發事件才會有一個 _args 的參數,並直接傳給回調函數。

再來看最后一個觸發函數 $.fn.trigger()

  $.fn.trigger = function(event, args){
    event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)
    event._args = args
    return this.each(function(){
      // handle focus(), blur() by calling them directly
      if (event.type in focus && typeof this[event.type] == "function") this[event.type]()
      // items in the collection might not be DOM elements
      else if ('dispatchEvent' in this) this.dispatchEvent(event)
      else $(this).triggerHandler(event, args)
    })
  }

這幾乎已經沒什么好分析的了,很簡單的邏輯。可以糾結一下 this 指針,最外層的 this 指向調用 trigger() 的 Zepto 集合,而經過 .each() 之后指向的一般是單個 DOM 元素。如果事件的類型是 focus / blur 的話,可以直接調用 DOM 元素的原生方法,其他情況則通過 DOM 來派發事件。而如果 this 不是一個 DOM 元素,則由 Zepto 包裹一次來直接觸發與其相關聯的句柄對象。

常用事件的快捷方法

對於一些常用事件,我們更希望采用如 el.click()el.error(()=>false) 的方法,而不是寫一大串的 el.on(...)el.bind(...) 等。

  // shortcut methods for `.bind(event, fn)` for each event type
  ;('focusin focusout focus blur load resize scroll unload click dblclick '+
  'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+
  'change select keydown keypress keyup error').split(' ').forEach(function(event) {
    $.fn[event] = function(callback) {
      return (0 in arguments) ?
        this.bind(event, callback) :
        this.trigger(event)
    }
  })

對於這些事件名,會在 Zepto 的原型上掛載相應的事件方法,如果不傳參數,則達到觸發事件的效果,傳入一個回調函數,則為調用元素的該事件注冊一個監聽。

系列相關

一個普通的 Zepto 源碼分析(一) - ie 與 form 模塊
一個普通的 Zepto 源碼分析(二) - ajax 模塊
一個普通的 Zepto 源碼分析(三) - event 模塊




本文基於 知識共享許可協議知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 發布,歡迎引用、轉載或演繹,但是必須保留本文的署名 BlackStorm 以及本文鏈接 http://www.cnblogs.com/BlackStorm/p/Zepto-Analysing-For-Event-Module.html ,且未經許可不能用於商業目的。如有疑問或授權協商請 與我聯系


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM