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è)條件:
句柄必須存在
如果
event.e
存在,則句柄的事件名必須與event
的事件名一致如果命名空間存在,則句柄的命名空間必須要與事件的命名空間匹配(
matcherFor
的作用 )如果指定匹配的事件句柄為
fn
,則當(dāng)前句柄handler
的_zid
必須與指定的句柄fn
相一致如果指定選擇器
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
對象中添加了 isDefaultPrevented
、isImmediatePropagationStopped
、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