为什么要看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。