Zepto源码分析——事件模块

Zepto的事件模块有些简化,因为Zepto作为一个针对移动端游览器的框架,所以游览器的事件系统本身相对完善,不需要做过多事件机制中最复杂的兼容处理。所以在分析后打算总结下针对PC端框架中兼容处理的。

游览器事件API

dom提供了三种层级的事件api:html事件、dom0级事件、dom2级事件(可以绑定多个回调)。整个事件机制主要简历在dom0级与dom2级两个标准上:http://bugzhang.com/2017/08/13/chang-yong-de-javascriptdai-ma-duan-2-dom-shi-jian-ajax-cookie/ ,改博文里第一段代码就是相关的实现,此处不再阐述。

与dom 0级事件的缺陷

  • 对于DOM3新增事件不支持,如:FocusIn,FocsuOut,MouseRemoved,MouseScrill等,但这些事件用的很少
  • 每次只可以绑定一个回调,重复绑定就会取消掉上次的绑定
  • 在ie下回调没有参数,在其他游览器回调第一个参数是事件对象
  • 只能再冒泡阶段可用

ie事件addachEvent的缺陷

  • this指向的是window,存在内存泄漏
  • 多钟时间绑定回调后,执行顺序不是按照绑定时的顺序触发
  • 与W3C有一些事件有区别
  • 只支持冒泡阶段

addEventListenner的缺陷

  • 部分游览器与标准的事件定义不一致
  • 第四个参数是ff跨文档监听事件,第五个参数是flash下制定监听函数的引用强弱
  • 事件对象不稳定,各个游览器有区别
  • input事件不如ie的propertychange事件好用

Zepto的Event模块核心方法

add:内部用来添加事件的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function add(element, events, fn, data, selector, delegator, capture) {
//拓展标识属性zid
var id = zid(element),
set = (handlers[id] || (handlers[id] = []))
events
//匹配空格,获取多个event
.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)
//如果支持dom2级事件
if ('addEventListener' in element)
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
}

remove:内部删除事件的方法

1
2
3
4
5
6
7
8
9
function remove(element, events, fn, selector, capture) {
var id = zid(element)
eachEvent(events || '', fn, function(event, fn) {
findHandlers(element, event, fn, selector).forEach(function(handler) {
delete handlers[id][handler.i]
element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
})
}

就是调用removeEventListener

on:添加事件

on是zepto中最核心的事件方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
$.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){
//如果是有one=true,先删掉事件,再执行事件
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)
})
}

参数说明:

  • event:事件类型,可以通过空格的字符串方式添加(“click mousedown”),或者事件类型为键,函数为值的方式({click:function,mousedown:function})。
  • selector:可选参数,事件委托的节点选择器
  • data:事件处理程序中的event.data属性
  • callback:事件处理程序的回调函数
  • one:绑定事件后,只触发一次回调

使用方式:

1
2
3
4
5
6
7
8
9
var elem = $('#content')
// observe all clicks inside #content:
elem.on('click', function(e){ ... })
// observe clicks inside navigation links in #content
elem.on('click', 'nav a', function(e){ ... })
// all clicks inside links in the document
$(document).on('click', 'a', function(e){ ... })
// disable following any navigation link on the page
$(document).on('click', 'nav a', false)

很多其他api是内部通过调用on方法实现的,如:

1
2
3
4
5
6
7
8
9
10
11
12
//基于一组特定的根元素为所有选择器匹配的元素附加一个处理事件,匹配的元素可能现在或将来才创建。
$.fn.delegate = function(selector, event, callback){
return this.on(event, selector, callback)
}
//添加一个处理事件到元素,当第一次执行事件以后,该事件将自动解除绑定,保证处理函数在每个元素上最多执行一次。
$.fn.one = function(event, selector, data, callback){
return this.on(event, selector, data, callback, 1)
}
//为一个元素绑定一个处理事件。
$.fn.bind = function(event, data, callback){
return this.on(event, data, callback)
}

off:移除事件

off模块也是主要的核心模块,主要做移除事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$.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

if (callback === false) callback = returnFalse

return $this.each(function(){
remove(this, event, callback, selector)
})
}

一些api是内部通过调用off方法实现的,如:

1
2
3
4
5
6
7
8
//移除Bind绑定的事件
$.fn.unbind = function (event, callback) {
return this.off(event, callback)
}
//移除通过delegate 注册的事件。
$.fn.undelegate = function (selector, event, callback) {
return this.off(event, selector, callback)
}

trigge与triggerHandler

在对象集合的元素上触发指定的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$.fn.trigger = function (event, args) {
event = (isString(event) || $.isPlainObject(event))
? $.Event(event)
: compatible(event)
event._args = args
return this.each(function () {
// handle focus(), blur() by calling them directly
if (event.type in focus && typeof this[event.type] == "function")
this[event.type]()
// items in the collection might not be DOM elements
else if ('dispatchEvent' in this)
this.dispatchEvent(event)
else
$(this).triggerHandler(event, args)
})
}
$.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
}

triggle模拟整个冒泡过程,除了自身,还触发祖先节点与window的同类型的回调,在游览器底层上,使用的方法是dispatchEvent,如果是在ie上是fireEvent。

$.Event:自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$.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)
}

底层采用的是游览器的createEvent和initEvent方法来模拟事件,从而实现自定义事件的功能。

一些补充

1.很多框架对于事件代理不采用捕获而采用冒泡的原因,是考虑兼容性

2.zepto因为自身主要做移动端的原因,所以对于低版本ie浏览器是不考虑的,我在上述中或多或少做了补充 3.zepto的很大一部分事件实际是在touch模块,这块主要是触摸事件,内容也比较多,打算留到下篇再写