Event 模塊是 Zepto 必備的模塊之一,由于對 Event Api 不太熟,Event 對象也比較復(fù)雜,所以乍一看 Event 模塊的源碼,有點(diǎn)懵,細(xì)看下去,其實(shí)也不太復(fù)雜。

讀Zepto源碼系列文章已經(jīng)放到了github上,歡迎star: reading-zepto

源碼版本

本文閱讀的源碼為 zepto1.2.0

準(zhǔn)備知識(shí)

focus/blur 的事件模擬

為什么要對 focus 和 blur 事件進(jìn)行模擬呢?從 MDN 中可以看到, focus 事件和 blur 事件并不支持事件冒泡。不支持事件冒泡帶來的直接后果是不能進(jìn)行事件委托,所以需要對 focus 和 blur 事件進(jìn)行模擬。

除了 focus 事件和 blur 事件外,現(xiàn)代瀏覽器還支持 focusin 事件和 focusout 事件,他們和 focus事件及 blur 事件的最主要區(qū)別是支持事件冒泡。因此可以用 focusin 和模擬 focus 事件的冒泡行為,用 focusout 事件來模擬 blur 事件的冒泡行為。

我們可以通過以下代碼來確定這四個(gè)事件的執(zhí)行順序:

<input id="test" type="text" />
const target = document.getElementById('test')target.addEventListener('focusin', () => {console.log('focusin')})target.addEventListener('focus', () => {console.log('focus')})target.addEventListener('blur', () => {console.log('blur')})target.addEventListener('focusout', () => {console.log('focusout')})

在 chrome59下, input 聚焦和失焦時(shí),控制臺(tái)會(huì)打印出如下結(jié)果:

'focus''focusin''blur''focusout'

可以看到,在此瀏覽器中,事件的執(zhí)行順序應(yīng)該是 focus > focusin > blur > focusout

關(guān)于這幾個(gè)事件更詳細(xì)的描述,可以查看:《說說focus /focusin /focusout /blur 事件

關(guān)于事件的執(zhí)行順序,我測試的結(jié)果與文章所說的有點(diǎn)不太一樣。感興趣的可以點(diǎn)擊這個(gè)鏈接測試下http://jsbin.com/nizugazamo/edit?html,js,console,output。不過我覺得執(zhí)行順序可以不必細(xì)究,可以將 focusin作為 focus 事件的冒泡版本。

mouseenter/mouseleave 的事件模擬

跟 focus 和 blur 一樣,mouseenter 和 mouseleave 也不支持事件的冒泡, 但是 mouseover 和 mouseout 支持事件冒泡,因此,這兩個(gè)事件的冒泡處理也可以分別用 mouseover 和 mouseout 來模擬。

在鼠標(biāo)事件的 event 對象中,有一個(gè) relatedTarget 的屬性,從 MDN:MouseEvent.relatedTarget 文檔中,可以看到,mouseover 的 relatedTarget 指向的是移到目標(biāo)節(jié)點(diǎn)上時(shí)所離開的節(jié)點(diǎn)( exited from ),mouseout 的 relatedTarget 所指向的是離開所在的節(jié)點(diǎn)后所進(jìn)入的節(jié)點(diǎn)( entered to )。

另外 mouseover 事件會(huì)隨著鼠標(biāo)的移動(dòng)不斷觸發(fā),但是 mouseenter 事件只會(huì)在進(jìn)入節(jié)點(diǎn)的那一刻觸發(fā)一次。如果鼠標(biāo)已經(jīng)在目標(biāo)節(jié)點(diǎn)上,那 mouseover 事件觸發(fā)時(shí)的 relatedTarget 為當(dāng)前節(jié)點(diǎn)。

因此,要模擬 mouseenter 或 mouseleave 事件,只需要確定觸發(fā) mouseover 或 mouseout 事件上的 relatedTarget 不存在,或者 relatedTarget 不為當(dāng)前節(jié)點(diǎn),并且不為當(dāng)前節(jié)點(diǎn)的子節(jié)點(diǎn),避免子節(jié)點(diǎn)事件冒泡的影響。

關(guān)于 mouseenter 和 mouseleave 的模擬, 謙龍 有篇文章《mouseenter與mouseover為何這般糾纏不清?》寫得很清楚,建議讀一下。

Event 模塊的核心

將 Event 模塊簡化后如下:

;(function($){})(Zepto)

其實(shí)就是向閉包中傳入 Zepto 對象,然后對 Zepto 對象做一些擴(kuò)展。

在 Event 模塊中,主要做了如下幾件事:

  • 提供簡潔的API

  • 統(tǒng)一不同瀏覽器的 event 對象

  • 事件句柄緩存池,方便手動(dòng)觸發(fā)事件和解綁事件。

  • 事件委托

內(nèi)部方法

zid

var _zid = 1function zid(element) {
  return element._zid || (element._zid = _zid++)}

獲取參數(shù) element 對象的 _zid 屬性,如果屬性不存在,則全局變量 _zid 增加 1 ,作為 element 的 _zid 的屬性值返回。這個(gè)方法用來標(biāo)記已經(jīng)綁定過事件的元素,方便查找。

parse

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

在 zepto 中,支持事件的命名空間,可以用 eventType.ns1.ns2... 的形式來給事件添加一個(gè)或多個(gè)命名空間。

parse 函數(shù)用來分解事件名和命名空間。

'' + event 是將 event 變成字符串,再以 . 分割成數(shù)組。

返回的對象中,e 為事件名, ns 為排序后,以空格相連的命名空間字符串,形如 ns1 ns2 ns3 ... 的形式。

matcherFor

function matcherFor(ns) {
  return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')}

生成匹配命名空間的表達(dá)式,例如,傳進(jìn)來的參數(shù) ns 為 ns1 ns2 ns3 ,最終生成的正則為 /(?:^| )ns1.* ?ns2.* ?ns3(?: |$)/。至于有什么用,下面馬上講到。

findHandlers,查找緩存的句柄

handlers = {}function findHandlers(element, event, fn, selector) {
  event = parse(event)  if (event.ns) var matcher = matcherFor(event.ns)  return (handlers[zid(element)] || []).filter(function(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)  })}

查找元素對應(yīng)的事件句柄。

event = parse(event)

調(diào)用 parse 函數(shù),分隔出 event 參數(shù)的事件名和命名空間。

if (event.ns) var matcher = matcherFor(event.ns)

如果命名空間存在,則生成匹配該命名空間的正則表達(dá)式 matcher。

return (handlers[zid(element)] || []).filter(function(handler) {
    ...  })

返回的其實(shí)是 handlers[zid(element)] 中符合條件的句柄函數(shù)。 handlers 是緩存的句柄容器,用 element 的 _zid 屬性值作為 key 。

javascript return handler // 條件1 && (!event.e || handler.e == event.e) // 條件2 && (!event.ns || matcher.test(handler.ns)) // 條件3 && (!fn || zid(handler.fn) === zid(fn)) // 條件4 && (!selector || handler.sel == selector) // 條件5

返回的句柄必須滿足5個(gè)條件:

  1. 句柄必須存在

  2. 如果 event.e 存在,則句柄的事件名必須與 event 的事件名一致

  3. 如果命名空間存在,則句柄的命名空間必須要與事件的命名空間匹配( matcherFor 的作用 )

  4. 如果指定匹配的事件句柄為 fn ,則當(dāng)前句柄 handler 的 _zid 必須與指定的句柄 fn 相一致

  5. 如果指定選擇器 selector ,則當(dāng)前句柄中的選擇器必須與指定的選擇器一致

從上面的比較可以看到,緩存的句柄對象的形式如下:

{
  fn: '', // 函數(shù)
  e: '', // 事件名
  ns: '', // 命名空間
  sel: '',  // 選擇器
  // 除此之外,其實(shí)還有
  i: '', // 函數(shù)索引
  del: '', // 委托函數(shù)
  proxy: '', // 代理函數(shù)
  // 后面這幾個(gè)屬性會(huì)講到}

realEvent,返回對應(yīng)的冒泡事件

focusinSupported = 'onfocusin' in window,focus = { focus: 'focusin', blur: 'focusout' },hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }function realEvent(type) {
  return hover[type] || (focusinSupported && focus[type]) || type}

這個(gè)函數(shù)其實(shí)是將 focus/blur 轉(zhuǎn)換成 focusin/focusout ,將 mouseenter/mouseleave 轉(zhuǎn)換成 mouseover/mouseout 事件。

由于 focusin/focusout 事件瀏覽器支持程度還不是很好,因此要對瀏覽器支持做一個(gè)檢測,如果瀏覽器支持,則返回,否則,返回原事件名。

compatible,修正event對象

returnTrue = function(){return true},returnFalse = function(){return false},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    })    try {
      event.timeStamp || (event.timeStamp = Date.now())    } catch (ignored) { }

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

compatible 函數(shù)用來修正 event 對象的瀏覽器差異,向 event 對象中添加了 isDefaultPreventedisImmediatePropagationStopped、isPropagationStopped 幾個(gè)方法,對不支持 timeStamp 的瀏覽器,向 event 對象中添加 timeStamp 屬性。

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 的 isDefaultPrevented 不存在時(shí)成立。

如果 source 不存在,則將 event 賦值給 source, 作為原事件對象。

遍歷 eventMethods ,獲得原事件對象的對應(yīng)方法名 sourceMethod。

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

改寫 event 對象相應(yīng)的方法,如果執(zhí)行對應(yīng)的方法時(shí),先將事件中方法所對應(yīng)的新方法賦值為 returnTrue 函數(shù) ,例如執(zhí)行 preventDefault 方法時(shí), isDefaultPrevented 方法的返回值為 true。

event[predicate] = returnFalse

這是將新添加的屬性,初始化為 returnFalse 方法

try {
  event.timeStamp || (event.timeStamp = Date.now())} catch (ignored) { }

這段向不支持 timeStamp 屬性的瀏覽器中添加 timeStamp 屬性。

if (source.defaultPrevented !== undefined ? source.defaultPrevented :    'returnValue' in source ? source.returnValue === false :    source.getPreventDefault && source.getPreventDefault())  event.isDefaultPrevented = returnTrue
  }

這是對瀏覽器 preventDefault 不同實(shí)現(xiàn)的兼容。

source.defaultPrevented !== undefined ? source.defaultPrevented : '三元表達(dá)式'

如果瀏覽器支持 defaultPrevented, 則返回 defaultPrevented 的值

'returnValue' in source ? source.returnValue === false : '后一個(gè)判斷'

returnValue 默認(rèn)為 true,如果阻止了瀏覽器的默認(rèn)行為, returnValue 會(huì)變?yōu)?nbsp;false 。

source.getPreventDefault && source.getPreventDefault()

如果瀏覽器支持 getPreventDefault 方法,則調(diào)用 getPreventDefault() 方法獲取是否阻止瀏覽器的默認(rèn)行為。

判斷為 true 的時(shí)候,將 isDefaultPrevented 設(shè)置為 returnTrue 方法。

createProxy,創(chuàng)建代理對象

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)}

zepto 中,事件觸發(fā)的時(shí)候,返回給我們的 event 都不是原生的 event 對象,都是代理對象,這個(gè)就是代理對象的創(chuàng)建方法。

ignoreProperties 用來排除 A-Z 開頭,即所有大寫字母開頭的屬性,還有以returnValue 結(jié)尾,layerX/layerY ,webkitMovementX/webkitMovementY 結(jié)尾的非標(biāo)準(zhǔn)屬性。

for (key in event)  if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]

遍歷原生事件對象,排除掉不需要的屬性和值為 undefined 的屬性,將屬性和值復(fù)制到代理對象上。

最終返回的是修正后的代理對象

eventCapture

function eventCapture(handler, captureSetting) {
  return handler.del &&
    (!focusinSupported && (handler.e in focus)) ||
    !!captureSetting}

返回 true 表示在捕獲階段執(zhí)行事件句柄,否則在冒泡階段執(zhí)行。

如果存在事件代理,并且事件為 focus/blur 事件,在瀏覽器不支持 focusin/focusout 事件時(shí),設(shè)置為 true , 在捕獲階段處理事件,間接達(dá)到冒泡的目的。

否則作用自定義的 captureSetting 設(shè)置事件執(zhí)行的時(shí)機(jī)。

add,Event 模塊的核心方法

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))      http://www.cnblogs.com/hefty/p/7198494.html