zepto源码分析·event模块

时间:2023-03-08 23:01:14
zepto源码分析·event模块

准备知识

事件的本质就是发布/订阅模式,dom事件也不例外;先简单说明下发布/订阅模式,dom事件api和兼容性

发布/订阅模式

所谓发布/订阅模式,用一个形象的比喻就是买房的人订阅楼房消息,售楼处发布消息,体现为代码的话就是如下形式

var Observable = {
callbacks: [],
add: function(fn) {
this.callbacks.push(fn);
},
fire: function(data) {
this.callbacks.forEach(function(fn) {
fn(data);
})
}
} //甲-订阅楼盘消息
Observable.add(function(data) {
show('执行动作一 ' + data)
})
//乙-订阅楼盘消息
Observable.add(function(data) {
show('执行动作二' + data)
}) // 售楼处-发布消息
Observable.fire(data);

DOM/Event

eventType   事件类型值
canBubble 事件是否起泡
cancelable 是否可以用preventDefault()方法取消事件 var event = document.createEvent(eventType); // 获取事件对象
event.initEvent(canBubble, canBubble, cancelable); // 初始化事件
dom.dispatchEvent(event); // 发送事件 dom.addEventListener(eventType, 处理方法, canBubble); // 绑定事件
dom.removeEventListener(eventType, 处理方法, canBubble); // 移除事件

兼容性

1.focus/blur和mouseenter/mouseleave事件不支持冒泡,因而不能进行事件委托,需要对其进行事件模拟,现代游览器的focusin和focusout和mouseover/mouseout刚好能做到

2.在鼠标事件中,有一个relatedTarget属性,返回与事件的目标节点相关的节点;

3.mouseover的relatedTarget指向移到目标节点上时离开的节点,mouseout的relatedTarget指向离开所在节点后进入的节点

4.要模拟mouseenter/mouseleave,只需确定触发mouseover/mouseout上的relatedTarget不存在,或者relatedTarget不为当前节点且不为当前节点的子节点(避免子节点事件冒泡的影响)

前要知识了解得差不多了,下面我们来看看zepto的event实现

事件创建/代理

$.Event

使用

// 创建一个点击事件,并允许事件冒泡
$.Event('click', { bubbles: true })

源码

// zepto代码很喜欢简写成一行, 为了可读性, 我修改如下
$.Event = function(type, props) {
// 如果type不为字符串类型,做一些处理
if (!isString(type)) {
props = type;
type = props.type;
}
// 创建原生事件, 如果specialEvents[type]不到预设类型,就用Events
var event = document.createEvent(specialEvents[type] || 'Events');
// 默认事件冒泡
var bubbles = true;
// 如果存在props,来一波Event对象设置
if (props) {
for (var name in props) {
if (name == 'bubbles') {
bubbles = !!props[name];
} else {
event[name] = props[name];
}
}
}
// 初始化事件配置
event.initEvent(type, bubbles, true);
// 对事件做一些改造和兼容性处理
return compatible(event);
}

可以看到,$.Event方法的实现利用了dom原生事件api,做了一些参数处理和事件对象的包装

$.proxy

使用

var obj = {name: 'Zepto'},
var handler = function(){
console.log("hello from + ", this.name)
}
$(document).on('click', $.proxy(handler, obj));

$.proxy方法的作用就是为了改变回调函数的this指向,下面看它的实现

$.proxy = function(fn, context) {
// 如果arguments参数超过2个,就说明存在代理参数
var args = (2 in arguments) && slice.call(arguments, 2)
if (isFunction(fn)) {
// 如果fn是方法,返回代理函数proxyFn
var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
proxyFn._zid = zid(fn)
return proxyFn
} else if (isString(context)) {
// 如果context为字符串,则将fn当做对象处理
if (args) {
// 有参数时,则组装args,重新执行$.proxy方法
args.unshift(fn[context], fn)
return $.proxy.apply(null, args)
} else {
return $.proxy(fn[context], fn)
}
} else {
throw new TypeError("expected function")
}
}

其中var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }可以整理成如下形式:

var proxyFn = function () {
var proxyArgs;
if (args) {
// 如果调用的时候,传递了参数
// 就跟执行$.proxy时可能传递了的参数做整合
proxyArgs = args.concat(slice.call(arguments));
} else {
// 直接取调用时传递的参数
proxyArgs = arguments;
}
return proxyArgs;
}

$.proxy方法其实就是做了原生bind方法做的事情,只是处理了做了类型判断处理

事件绑定/解除

on

使用

var elem = $('#content');
elem.on('click', function(e){ ... });
elem.on({ type: handler, type2: handler2, ... }, 'li');

源码

// 将on绑定方法定义在zepto原型上
$.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 // 如果callback为false,直接赋值会返回retunr false的函数
if (callback === false) callback = returnFalse // 为每个dom元素执行事件绑定处理
return $this.each(function(_, element){
// 如果存在one参数,设置只执行一次操作的函数
if (one) autoRemove = function(e){
remove(element, e.type, callback)
return callback.apply(this, arguments)
} // 如果存在selector参数,创建委托函数
if (selector) delegator = function(e){ // 通过closest方法找到代理元素
// 这也就是绑定未来才出现的元素的秘诀,不是考刷定时器检测未来元素
// 而是当事件触发,代理delegator方法被事件回调执行时进行动态获取
var evt, match = $(e.target).closest(selector, element).get(0)
if (match && match !== element) { // 通过内部createProxy方法创建代理事件对象, 排除非标准属性
// 然后将代理元素和触发事件的元素保存到事件对象中
evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) // 传入的事件回调函数被执行
return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
}
} // 进行zepto版本的订阅操作,也就是对事件和传入的回调函数进行绑定
add(element, event, callback, data, selector, delegator || autoRemove)
})
}

add实现如下

function add(element, events, fn, data, selector, delegator, capture){
// zid方法用于在elementt添加标记序号,便于内部对事件绑定内容进行查找
// handlers为内部定义的对象字面量, 用于根据id存储绑定内容,充当句柄对象容器(内部缓存对象)
var id = zid(element), set = (handlers[id] || (handlers[id] = [])) // 以空格进行拆分,遍历各种事件进行绑定
events.split(/\s/).forEach(function(event){
// 如果为ready, 走ready流程
if (event == 'ready') return $(document).ready(fn) // 创建回调句柄, parse方法生成基础句柄对象
var handler = parse(event)
handler.fn = fn
handler.sel = selector // 对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)
// 如果调用了event.stopImmediatePropagation()
if (e.isImmediatePropagationStopped()) return
e.data = data
var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
// 如果事件回调执行了return false;
if (result === false) e.preventDefault(), e.stopPropagation()
return result
}
handler.i = set.length // 将句柄对象添加到内部句柄容器handlers, 也就是订阅操作
set.push(handler) // 具体绑定操作
if ('addEventListener' in element)
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
}

对focus/blur和mouseenter/mouseleave的模拟和兼容处理

// 顶部变量定义
focusinSupported = 'onfocusin' in window,
focus = { focus: 'focusin', blur: 'focusout' },
hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' } // 事件传递的形式(捕获/冒泡)
// 如果事件为focus/blur事件并且浏览器不支持focusin/focusout时, 设置为true
// 为true时, 在捕获阶段处理事件,间接达到冒泡的目的
// captureSetting参数在内部并未传递, 通过!!自动转换为false, 做默认值
function eventCapture(handler, captureSetting) {
return handler.del &&
(!focusinSupported && (handler.e in focus)) ||
!!captureSetting
} // 将focus/blur转换成focusin/focusout,将mouseenter/mouseleave转换成mouseover/mouseout
function realEvent(type) {
return hover[type] || (focusinSupported && focus[type]) || type
}

off

使用

var elem = $('#content'),
var callback = function() {};
elem.off('click', callback);
elem.off({ type: handler, type2: handler2, ... }, 'li');

源码

$.fn.off = function(event, selector, callback){
var $this = this
// 处理{'事件类型':'函数'}的传参形式
if (event && !isString(event)) {
$.each(event, function(type, fn){
$this.off(type, selector, fn)
})
return $this
} // 处理非{'事件类型':'函数'}的传参形式
if (!isString(selector) && !isFunction(callback) && callback !== false)
callback = selector, selector = undefined // 如果callback为false,直接赋值会返回retunr false的函数
if (callback === false) callback = returnFalse // 为每个dom元素执行事件解除处理
return $this.each(function(){
remove(this, event, callback, selector)
})
}

remove实现如下

function remove(element, events, fn, selector, capture){
// 通过zid方法找回元素的标记序号
var id = zid(element) // 以空格进行拆分,遍历各种事件进行解除
;(events || '').split(/\s/).forEach(function(event){ // 通过findHandlers方法(操作handlers对象)找回对应的函数句柄,遍历进行绑定解除
findHandlers(element, event, fn, selector).forEach(function(handler){
delete handlers[id][handler.i]
if ('removeEventListener' in element)
element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
})
}

bind/unbind

$.fn.bind = function(event, data, callback){
return this.on(event, data, callback)
}
$.fn.unbind = function(event, callback){
return this.off(event, callback)
}

live/die

所谓给未来的元素绑定,换汤不换药,都是先通过绑定一个元素,然后执行时再具体查找子元素,再执行回调

$.fn.live = function(event, callback){
$(document.body).delegate(this.selector, event, callback)
return this
}
$.fn.die = function(event, callback){
$(document.body).undelegate(this.selector, event, callback)
return this
}

delegate/undelegate

$.fn.delegate = function(selector, event, callback){
return this.on(event, selector, callback)
}
$.fn.undelegate = function(selector, event, callback){
return this.off(event, selector, callback)
}

one

$.fn.one = function(event, selector, data, callback){
return this.on(event, selector, data, callback, 1)
}

事件触发

trigger

使用

$(document.body).trigger('click', 'hello');

源码

$.fn.trigger = function(event, args){
// 字符串和纯粹Object对象判断, 如果是就创建Event对象, 否则返回处理后的event对象
event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event) // 将参数保存到事件对象上
event._args = args // 遍历所有dom元素进行触发操作
return this.each(function(){ // 如果是focus/blur事件, 则直接执行, focus/blur游览器原生支持
if (event.type in focus && typeof this[event.type] == "function") this[event.type]() // 存在dispatchEvent则直接执行事件触发
else if ('dispatchEvent' in this) this.dispatchEvent(event) // 不存在dispatchEvent时, 则直接执行triggerHandler
else $(this).triggerHandler(event, args)
})
}

triggerHandler

使用

$(this).triggerHandler('click', '我是参数');

源码

$.fn.triggerHandler = function(event, args){
var e, result // 遍历所有dom元素进行触发操作
this.each(function(i, element){ // 通过内部createProxy方法创建代理事件对象, 排除非标准属性
// 字符串判断, 如果是就创建Event对象, 否则返回处理后的event对象
e = createProxy(isString(event) ? $.Event(event) : event) // 将参数保存到事件对象上
e._args = args // 将element保存到事件对象上
e.target = element // 通过findHandlers方法(操作handlers对象)找回对应的函数句柄,遍历进行执行
$.each(findHandlers(element, event.type || event), function(i, handler){
result = handler.proxy(e)
if (e.isImmediatePropagationStopped()) return false
})
})
return result
}

事件判断

isDefaultPrevented

如果preventDefault()被该事件的实例调用,那么返回true

isImmediatePropagationStopped

如果stopImmediatePropagation()被该事件的实例调用,那么返回true

isPropagationStopped

如果stopPropagation()被该事件的实例调用,那么返回true

方法实现依赖于内部, 源码如下

// 顶部变量定义
var 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) // 遍历eventMethods进行原生函数劫持和事件判断函数的支持
$.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()) // 是否调用了event.preventDefault()方法
// 是否存在新的defaultPrevented判断属性,存在就用它
// 不存在就用非标准属性returnValue,存在就用它
// 不存在就用老属性方法getPreventDefault
if (source.defaultPrevented !== undefined ? source.defaultPrevented :
'returnValue' in source ? source.returnValue === false :
source.getPreventDefault && source.getPreventDefault())
event.isDefaultPrevented = returnTrue
}
return event
}

上述就Zepto对事件模块的实现,大约300行的样子