Zepto原始碼學習Event模組

B_Cornelius發表於2018-08-03

為什麼要看Zepto的原始碼,因為公司用的是這個。。。。
再看這個原始碼的過程中,因為對事件型別的不充分,導致學習起來有些費勁,所以在講這個板塊之前先對一些事件進行了解。

瞭解基本event資訊

事件分發

下面是觸發點選事件的程式碼,我們在inner上新增點選事件,在wrapper新增事件,點選inner都會觸發click事件。但這種情況需要我們每次都去點選回撥函式才會執行,有沒有函式不需要我們手動觸發,自動觸發呢?

<div class="wrapper">
  wrapper
  <div class="inner">inner</div>
</div>
<script>
  var $inner = document.getElementById('inner');
  var $wrapper = document.getElementById('wrapper');

  $inner.addEventListener('click', function () {
    console.log(this.innerHTML);
  });

  $wrapper.addEventListener('click', function () {
    console.log(this.innerHTML);
  });
  </script>
複製程式碼

這裡用到了一個需要用到一個API: createEvent,具體程式碼如下:

  let event = document.createEvent('Event')
  event.initEvent('click', true, true)
  $inner.dispatchEvent(event)
複製程式碼

這裡我們通過createEvent建立了一個事件,並且其後必須馬上進行初始化,然後通過dispatchEvent進行事件分發,這樣就用js程式碼進行事件的觸發,而不需要我們進行點選才能觸發。

事件模擬

在event模組中有這麼一段程式碼

focus = {focus: 'focusin', blur: 'focusout'},
hover = {mouseenter: 'mouseover', mouseleave: 'mouseout'}
複製程式碼

focus和blur我們都知道,但是為什麼要重新隱射focusin和blur事件呢,在mdn中我們可以看到focus和focusin的區別在於focus不支援事件冒泡,如果不支援事件冒泡,那麼帶來的效果就是不能夠進行事件委託。
同樣的mouseenter和mouseleave也不支援事件冒泡,但是mouseenter會帶來巨大的效能消耗,所以我們常用mouseover進行mouseenter進行事件的模擬。在滑鼠事件中,有一個relatedTarget事件,在前面提到因為mouseover支援冒泡,那該如何來模擬mouseenter事件呢。relatedTarget事件屬性返回的是和事件的目標節點相關的節點。對於mouseover事件來說,該屬性是滑鼠指標移到目標節點上所離開的那個節點。對於mouseout事件來說,該屬性是離開目標時,滑鼠進入的節點。根據上面的描述,我們可以對relatedTarget的值進行判斷:如果值不是目標元素,也不是目標元素的子元素,就說明滑鼠已經移入目標元素而不是在元素內部移動

核心程式碼

zid

var _zid = 1
function zid(element) {
  return element._zid || (element._zid = zid++)
}
複製程式碼

zid主要是用來標記已經繫結時間的元素,這個函式返回元素的_zid,如果沒有,那就全域性的zid加一,並且賦值給元素的_zid屬性

parse

  function parse(event) {
    var parts=('' + event).split('.')
    return {
      e: parts[0], ns: parts.slice(1).sort().join(' ')
    }
  }
複製程式碼

parse方法用來分解事件名和名稱空間,{e: 事件名, ns: 名稱空間},先把event變成字串進行分割,得到事件名,和名稱空間,名稱空間可以為s1.s2.s3這種

compatible

這是用來修正event物件中瀏覽器的差異

eventMethods = {
  preventDefault: 'isDefaultPrevented',
  stopImmediatePropagation: 'isImmediatePropagationStopped',
  stopPropagation: 'isPropagationStopped'
}
function compatible(event, source) {
  if (source || !event.isDefaultPrevented) {
    source || (source = event)

    $.each(eventMethods, function(name, predicate) {
      var sourceMethod = source[name]
      event[name] = function(){
        this[predicate] = returnTrue
        return sourceMethod && sourceMethod.apply(source, arguments)
      }
      event[predicate] = returnFalse
    })

    event.timeStamp || (event.timeStamp = Date.now())

    if (source.defaultPrevented !== undefined ? source.defaultPrevented :
        'returnValue' in source ? source.returnValue === false :
        source.getPreventDefault && source.getPreventDefault())
      event.isDefaultPrevented = returnTrue
  }
  return event
}
複製程式碼

具體來看看他的程式碼

if (source || !event.isDefaultPrevented) {
    source || (source = event)
複製程式碼

如果原事件存在,或者事件event的isDefaultPrevented為false或者不存在成立 如果原事件source不存在,就把event賦值給source

  $.each(eventMethod, function(name, predicate) {
    var sourceMethod = source[name]
    event[name] = function(){
      this[predicate] = returnTrue
      return sourceMethod && sourceMethod.apply(source, arguments)
    }
  }) 
複製程式碼

這裡是遍歷eventMethod,獲取原事件對應的方法名sourceMethod。對event事件進行重新賦值,先把方法賦值為returnTrue函式,返回執行原方法的返回值。

event[predicate] = returnFalse
複製程式碼

新新增的屬性初始化為returnFalse。

  event.timeStamp || (event.timeStamp = Date.now())
複製程式碼

看事件是否支援timeStamp,如果不支援,將Date.now()賦值給timeStamp,最後返回做了相容性處理的event。

createProxy

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,首先在proxy的originalEvent掛載本身,然後遍歷event,將event的屬性複製到proxy,最後返回對proxy和event做相容性處理。

add

// element 事件繫結的元素,events繫結的事件列表,fn事件執行時的控制程式碼,data傳遞給事件物件的資料
// 繫結元素的選擇器,delegator事件委託函式,capture哪個階段執行事件控制程式碼
function add(element, events, fn, data, selector, delegator, capture){
    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)
      handler.fn    = fn
      handler.sel   = selector
      // emulate mouseenter, mouseleave
      if (handler.e in hover) fn = function(e){
        var related = e.relatedTarget
        if (!related || (related !== this && !$.contains(this, related)))
          return handler.fn.apply(this, arguments)
      }
      handler.del   = delegator
      var callback  = delegator || fn
      handler.proxy = function(e){
        e = compatible(e)
        if (e.isImmediatePropagationStopped()) return
        e.data = data
        var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
        if (result === false) e.preventDefault(), e.stopPropagation()
        return result
      }
      handler.i = set.length
      set.push(handler)
      if ('addEventListener' in element)
        element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
    })
  }
複製程式碼

add方法主要是給元素新增事件和事件響應。

id = zid(element), set = (handlers[id] || (handlers[id] = []))
複製程式碼

獲取element的id,然後通過id來獲取他的控制程式碼容器

events.split(/\s/).forEach(function (event) {
  if (event == 'ready') return $(document).ready(fn)
})
複製程式碼

對events進行分解,如果event是ready就直接執行fn

var handler   = parse(event)
    handler.fn    = fn
    handler.sel   = selector
複製程式碼

對event進行事件名和名稱空間進行分離,然後將資訊掛載到handler上,handler的最終結構是這樣的:

{
  fn: '', // 函式
  e: '', // 事件名
  ns: '', // 名稱空間
  sel: '', // 選擇器
  i: '',  // 函式索引
  del: '', // 委託函式
  proxy: '' // 代理函式
}
複製程式碼

繼續看下面的

 if (handler.e in hover) {
   fn = function (e) {
     var related = e.relatedTarget;
     if (!related || (related !== this && !$.contains(this, related))) {
       return handler.fn.apply(this, arguments)
     }
   }
 }
複製程式碼

這就是我們最先提到的mouseover和mouseenter事件,這裡就是對Hover事件進行判斷,如果related不存在,或者related不等於目標元素,並且不是目標元素的子元素,就能夠完成mouseenter的模擬,然後返回函式處理後的結果。

handler.proxy = function (e) {
  e = compatible(e);
  if (e.isImmediatePropagationStopped()) {
    return
  }
  e.data = data;
  var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
  if (result === false) {
    e.preventDefault();
    e.stopPropagation();
  }
  return result;
}
複製程式碼

首先對e進行相容處理,然後判斷是否阻止預設行為,如果是則啥都不做,把data掛載到event物件上,e是事件執行時的event物件,並且使用compatible對e進行修正。呼叫控制程式碼,並且返回處理結果。

set.push(handler)
if ('addEventListener' in element) 
  element.addEventListener(realEvent(hander.e), handler.proxy, eventCapture(handler, capture))
複製程式碼

向控制程式碼容器新增控制程式碼,並且給元素新增事件。

on

$.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)
    })
    return $this
  }

  if (!isString(selector) && !isFunction(callback) && callback !== false)
    callback = data, data = selector, selector = undefined
  if (callback === undefined || data === false)
    callback = data, data = undefined

  if (callback === false) callback = returnFalse

  return $this.each(function (_, element) {
    if (one) autoRemove = function (e) {
      remove(element, e.type, callback)
      return callback.apply(this, arguments)
    }

    if (selector) delegator = function (e) {
      var evt, match = $(e.target).closest(selector, element).get(0)
      if (match && match !== element) {
        evt = $.extend(createProxy(e), {
          currentTarget: match,
          liveFired: element
        })
        return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
      }
    }
    }
    }

    add(element, event, callback, data, selector, delegator || autoRemove)
  })
}
複製程式碼

on方法是給元素繫結事件,最後呼叫的add方法。

 var autoRemove, delegator, $this = this
 if (event && !isString(event)) {
   $.each(event, function (type, fn) {
     $this.on(type, selector, data, fn, one)
   })
   return $this
 }
複製程式碼

如果event不是字串,可能就是物件或者陣列,然後對其遍歷,每個都呼叫on函式。

  if (!isString(selector) && !isFunction(callback) && callback !== false)
      callback = data, data = selector, selector = undefined
    if (callback === undefined || data === false)
      callback = data, data = undefined
複製程式碼

這是針對引數不同的情況進行的操作

return $this.each(function (_, element) {
  if (one) 
  autoRemove = function (e) {
    remove(element, e.type, callback)
    return callback.apply(this, arguments)
  }
})
複製程式碼

如果one為true,autoRemove進行的操作是把元素上對應的事件進行解綁,並且呼叫回撥。

if (selector) 
  delegator = function (e) {
    var evt, match = $(e.target).closet(selector, element).get(0)
    if (match && match !== element) {
      evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
      return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
    }
  }
  add(element, event, callback, data, selector, delegator || autoRemove)
複製程式碼

如果selector,就需要做事件代理,呼叫closet找到最近selector的元素。如果match存在,並且不是當前元素,就呼叫createProxy(),給當前事件物件建立代理物件,在呼叫extend方法,為其擴充套件currentTarget和liveFired屬性,將代理元素和觸發元素儲存到事件物件中。 最後執行控制程式碼函式,match作為上下文,用代理後的event物件evt替換掉原控制程式碼函式的第一個函式。

triggerHandler

$.fn.triggerHandler = function (event, args) {
  var e, result;
  this.each(function(i, element) {
    e = createProxy(isString(event) ? $.Event(event) : event)
    e._args = args
    e.target = element
    $.each(findHandlers(element, event.type || event), function (i, handler) {
      result = handler.proxy(e);
      if (e.isImmediatePropagationStopped()) return false;
    })
  })
  return result;
}
複製程式碼

triggerHandler用於直接執行函式。

this.each(function(i, element) {
    e = createProxy(isString(event) ? $.Event(event) : event)
    e._args = args
    e.target = element
複製程式碼

遍歷元素,然後判斷event如果是字串則使用$.Event建立事件,然後使用createProxy建立代理。

$.each(findHandlers(element, event.type || event), function (i, handler) {
      result = handler.proxy(e);
      if (e.isImmediatePropagationStopped()) return false;
    })
複製程式碼

尋找元素上所有的控制程式碼,handler.proxy我們在之前提到過這是真的回撥函式,如果有isImmediatePropagationStopped,則終止遍歷。

Event

$.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)
}
複製程式碼

簡單來說這個部分就是建立事件,初始化事件,然後返回相容後的event。

參考文章:
mouseenter與mouseover為何這般糾纏不清?
讀Zepto原始碼之Event模組
Zepto文件

相關文章