Event 模块是 Zepto 必备的模块之一,由于对 Event Api 不太熟,Event 对象也比较复杂,所以乍一看 Event 模块的源码,有点懵,细看下去,其实也不太复杂。
读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto
源码版本
本文阅读的源码为 zepto1.2.0
准备知识
focus/blur 的事件模拟
为什么要对 focus
和 blur
事件进行模拟呢?从 MDN 中可以看到, focus
事件和 blur
事件并不支持事件冒泡。不支持事件冒泡带来的直接后果是不能进行事件委托,所以需要对 focus
和 blur
事件进行模拟。
除了 focus
事件和 blur
事件外,现代浏览器还支持 focusin
事件和 focusout
事件,他们和 focus
事件及 blur
事件的最主要区别是支持事件冒泡。因此可以用 focusin
和模拟 focus
事件的冒泡行为,用 focusout
事件来模拟 blur
事件的冒泡行为。
我们可以通过以下代码来确定这四个事件的执行顺序:
<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
聚焦和失焦时,控制台会打印出如下结果:
'focus''focusin''blur''focusout'
可以看到,在此浏览器中,事件的执行顺序应该是 focus > focusin > blur > focusout
关于这几个事件更详细的描述,可以查看:《说说focus /focusin /focusout /blur 事件》
关于事件的执行顺序,我测试的结果与文章所说的有点不太一样。感兴趣的可以点击这个链接测试下http://jsbin.com/nizugazamo/edit?html,js,console,output。不过我觉得执行顺序可以不必细究,可以将 focusin
作为 focus
事件的冒泡版本。
mouseenter/mouseleave 的事件模拟
跟 focus
和 blur
一样,mouseenter
和 mouseleave
也不支持事件的冒泡, 但是 mouseover
和 mouseout
支持事件冒泡,因此,这两个事件的冒泡处理也可以分别用 mouseover
和 mouseout
来模拟。
在鼠标事件的 event
对象中,有一个 relatedTarget
的属性,从 MDN:MouseEvent.relatedTarget 文档中,可以看到,mouseover
的 relatedTarget
指向的是移到目标节点上时所离开的节点( exited from
),mouseout
的 relatedTarget
所指向的是离开所在的节点后所进入的节点( entered to
)。
另外 mouseover
事件会随着鼠标的移动不断触发,但是 mouseenter
事件只会在进入节点的那一刻触发一次。如果鼠标已经在目标节点上,那 mouseover
事件触发时的 relatedTarget
为当前节点。
因此,要模拟 mouseenter
或 mouseleave
事件,只需要确定触发 mouseover
或 mouseout
事件上的 relatedTarget
不存在,或者 relatedTarget
不为当前节点,并且不为当前节点的子节点,避免子节点事件冒泡的影响。
关于 mouseenter
和 mouseleave
的模拟, 谦龙 有篇文章《mouseenter与mouseover为何这般纠缠不清?》写得很清楚,建议读一下。
Event 模块的核心
将 Event
模块简化后如下:
;(function($){})(Zepto)
其实就是向闭包中传入 Zepto
对象,然后对 Zepto
对象做一些扩展。
在 Event
模块中,主要做了如下几件事:
提供简洁的API
统一不同浏览器的
event
对象事件句柄缓存池,方便手动触发事件和解绑事件。
事件委托
内部方法
zid
var _zid = 1function zid(element) { return element._zid || (element._zid = _zid++)}
获取参数 element
对象的 _zid
属性,如果属性不存在,则全局变量 _zid
增加 1
,作为 element
的 _zid
的属性值返回。这个方法用来标记已经绑定过事件的元素,方便查找。
parse
function parse(event) { var parts = ('' + event).split('.') return {e: parts[0], ns: parts.slice(1).sort().join(' ')}}
在 zepto
中,支持事件的命名空间,可以用 eventType.ns1.ns2...
的形式来给事件添加一个或多个命名空间。
parse
函数用来分解事件名和命名空间。
'' + event
是将 event
变成字符串,再以 .
分割成数组。
返回的对象中,e
为事件名, ns
为排序后,以空格相连的命名空间字符串,形如 ns1 ns2 ns3 ...
的形式。
matcherFor
function matcherFor(ns) { return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')}
生成匹配命名空间的表达式,例如,传进来的参数 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) })}
查找元素对应的事件句柄。
event = parse(event)
调用 parse
函数,分隔出 event
参数的事件名和命名空间。
if (event.ns) var matcher = matcherFor(event.ns)
如果命名空间存在,则生成匹配该命名空间的正则表达式 matcher
。
return (handlers[zid(element)] || []).filter(function(handler) { ... })
返回的其实是 handlers[zid(element)]
中符合条件的句柄函数。 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个条件:
句柄必须存在
如果
event.e
存在,则句柄的事件名必须与event
的事件名一致如果命名空间存在,则句柄的命名空间必须要与事件的命名空间匹配(
matcherFor
的作用 )如果指定匹配的事件句柄为
fn
,则当前句柄handler
的_zid
必须与指定的句柄fn
相一致如果指定选择器
selector
,则当前句柄中的选择器必须与指定的选择器一致
从上面的比较可以看到,缓存的句柄对象的形式如下:
{ fn: '', // 函数 e: '', // 事件名 ns: '', // 命名空间 sel: '', // 选择器 // 除此之外,其实还有 i: '', // 函数索引 del: '', // 委托函数 proxy: '', // 代理函数 // 后面这几个属性会讲到}
realEvent,返回对应的冒泡事件
focusinSupported = 'onfocusin' in window,focus = { focus: 'focusin', blur: 'focusout' },hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }function realEvent(type) { return hover[type] || (focusinSupported && focus[type]) || type}
这个函数其实是将 focus/blur
转换成 focusin/focusout
,将 mouseenter/mouseleave
转换成 mouseover/mouseout
事件。
由于 focusin/focusout
事件浏览器支持程度还不是很好,因此要对浏览器支持做一个检测,如果浏览器支持,则返回,否则,返回原事件名。
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
函数用来修正 event
对象的浏览器差异,向 event
对象中添加了 isDefaultPrevented
、isImmediatePropagationStopped
、isPropagationStopped
几个方法,对不支持 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
不存在时成立。
如果 source
不存在,则将 event
赋值给 source
, 作为原事件对象。
遍历 eventMethods
,获得原事件对象的对应方法名 sourceMethod
。
event[name] = function(){ this[predicate] = returnTrue return sourceMethod && sourceMethod.apply(source, arguments)}
改写 event
对象相应的方法,如果执行对应的方法时,先将事件中方法所对应的新方法赋值为 returnTrue
函数 ,例如执行 preventDefault
方法时, 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
不同实现的兼容。
source.defaultPrevented !== undefined ? source.defaultPrevented : '三元表达式'
如果浏览器支持 defaultPrevented
, 则返回 defaultPrevented
的值
'returnValue' in source ? source.returnValue === false : '后一个判断'
returnValue
默认为 true
,如果阻止了浏览器的默认行为, returnValue
会变为 false
。
source.getPreventDefault && source.getPreventDefault()
如果浏览器支持 getPreventDefault
方法,则调用 getPreventDefault()
方法获取是否阻止浏览器的默认行为。
判断为 true
的时候,将 isDefaultPrevented
设置为 returnTrue
方法。
createProxy,创建代理对象
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
中,事件触发的时候,返回给我们的 event
都不是原生的 event
对象,都是代理对象,这个就是代理对象的创建方法。
ignoreProperties
用来排除 A-Z
开头,即所有大写字母开头的属性,还有以returnValue
结尾,layerX/layerY
,webkitMovementX/webkitMovementY
结尾的非标准属性。
for (key in event) if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]
遍历原生事件对象,排除掉不需要的属性和值为 undefined
的属性,将属性和值复制到代理对象上。
最终返回的是修正后的代理对象
eventCapture
function eventCapture(handler, captureSetting) { return handler.del && (!focusinSupported && (handler.e in focus)) || !!captureSetting}
返回 true
表示在捕获阶段执行事件句柄,否则在冒泡阶段执行。
如果存在事件代理,并且事件为 focus/blur
事件,在浏览器不支持 focusin/focusout
事件时,设置为 true
, 在捕获阶段处理事件,间接达到冒泡的目的。
否则作用自定义的 captureSetting
设置事件执行的时机。
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