event模块-zepto源码

前言

zepto是一个轻量级的类jQuery的JavaScript库,适合在移动端页面开发使用。轻量又提供许多dom操作API。

前面我们讲解了zepto模块,现在来看看event模块。

event模块

整体结构:
image

先看下面代码

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>event</title>
</head>
<body>
<div class="a"></div>
</body>
</html>
<script src="../../src/zepto.js"></script>
<script src="../../src/event.js"></script>
<script>
var obj = {name: 'Zepto'},
handler = function(){ console.log("hello from + ", this.name) }
$(document).on('click', $.proxy(handler, obj))
</script>

<script>
$.Event('myevent')

$(document).on('myevent',".a", function(e, from, to){
console.log('change on %o with data %s, %s', e.target, from, to)
})

$(".a").trigger('myevent', ['one', 'two'])
</script>

上面的实例代码,基本将event模块上的主要方法和内容包含了$.proxy(),$.Event(),on(),trigger()这些方法。在zepto的官方文档中,其明确不推荐我们用bind等方法绑定事件,其实bind调用是on()方法,所以用on()绑定事件更好。

$.proxy()

1
2
3
4
5
$.proxy v1.0+
$.proxy(fn, context) ⇒ function
$.proxy(fn, context, [additionalArguments...]) ⇒ function v1.1.4+
$.proxy(context, property) ⇒ function
$.proxy(context, property, [additionalArguments...]) ⇒ function v1.1.4+

接受一个函数,然后返回一个新函数,并且这个新函数始终保持了特定的上下文(context)语境,新函数中this指向context参数。另外一种形式,原始的function是从上下文(context)对象的特定属性读取。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$.proxy = function(fn, context) {
var args = (2 in arguments) && slice.call(arguments, 2)

if (isFunction(fn)) {
var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) }
proxyFn._zid = zid(fn)
return proxyFn
} else if (isString(context)) {
if (args) {
args.unshift(fn[context], fn)
return $.proxy.apply(null, args)
} else {
return $.proxy(fn[context], fn)
}
} else {
throw new TypeError("expected function")
}
}

源码的实现其实很简单,主要用的apply强绑定this执行上下文。不加细说。

$.Event()

执行这么一段代码console.log($.Event('myevent') ),这里返回一个Event对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Event对象
{
bubbles:是否冒泡
cancelBubble:是否可以取消冒泡
cancelable:是否可以取消时间
currentTarget:指向被绑定事件句柄(event handler)的元素
defaultPrevented:默认是否执行
isDefaultPrevented
isImmediatePropagationStopped
isPropagationStopped
isTrusted:表明当前事件是否是由用户行为触发
preventDefault:取消对象的默认事件
returnValue:事件句柄返回值
stopImmediatePropagation
stopPropagation:阻止冒泡
target:触发事件的对象
timeStamp:事件创建的时间
type:事件类型
}

那么$.Event()代码是如何执行的

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
44
45
$.Event = function(type, props) {
if (!isString(type)) {
// 如果type不为字符串
props = type, type = props.type
}

var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true
// 创建一个事件对象,type默认值为'Events',如果type为特殊事件类型,设为MouseEvents
// specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'

if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
event.initEvent(type, bubbles, true)
return compatible(event)
}

// 执行compatible(),完善event对象上的属性和方法。
function compatible(event, source) {
if (source || !event.isDefaultPrevented) {
source || (source = event)

// 为事件对象添加preventDefault,stopImmediatePropagation,stopPropagation和对应属性
$.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) { }
/*
* timeStamp:返回事件发生时的时间戳
* 兼容IE10及以上
*/

if (source.defaultPrevented !== undefined ? source.defaultPrevented :
'returnValue' in source ? source.returnValue === false :
source.getPreventDefault && source.getPreventDefault())
event.isDefaultPrevented = returnTrue
}
return event
}

细节说明

1
2
3
4
5
6
7
8
9
10
11
12
13

* document.createEvent :创建一个新的事件(Event),随之必须调用自身的 init 方法进行初始化。
* type指明待创建事件对象类型的字符串,此方法返回一个新的特定类型的DOM Event对象,在使用之前需要使用自身的initEvent()进行初始化。
var newEvent = document.createEvent("UIEvents");

* event.initEvent(type, bubbles, cancelable);初始化 使用document.createEvent返回的事件对象
* type:一个DOM字符串,用于定义事件的类型。bubbles:布尔值,指明是否冒泡。cancelable:布尔值,表示该事件是否可以被取消(指明event.cancelable属性值)
event.initEvent("click", true, false);

* EventTarget.dispatchEvent(event):向一个指定的EventTarget派发一个事件,以合适的顺序触发受影响的EventListeners
* event:要被派发的event事件;EventTarget是指被初始化的Event.target和决定触发event listeners。
* 当至少一个该事件的 event handler 调用了Event.preventDefault(),则返回值为false;否则返回true。
cancelled = !target.dispatchEvent(event)

on()绑定事件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
  /*
* event:事件字符串,selector:选择器字符串,被监听的dom对象,
* data;在事件执行过程作为有用event.data属性,
* 注:当事件处理函数返回false时,即为阻止默认事件执行。
*/
$.fn.on = function(event, selector, data, callback, one){
var autoRemove, delegator, $this = this
if (event && !isString(event)) {
// 如果event传入的数组,遍历执行,添加
$.each(event, function(type, fn){
$this.on(type, selector, data, fn, one)
})
return $this
}

if (!isString(selector) && !isFunction(callback) && callback !== false)
// on(type, [data], function(e){ ... })这种情况
callback = data, data = selector, selector = undefined

if (callback === undefined || data === false)
// $(document).on('click', 'nav a', false)
callback = data, data = undefined

if (callback === false)
// $(document).on('click', 'nav a',[data], 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)
})
}

// 执行add()
function add(element, events, fn, data, selector, delegator, capture){
// console.log(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)
// handler事件句柄对象
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数组:存储Handler对象
set.push(handler)
if ('addEventListener' in element)
// 绑定事件
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
})
  • 输出其中关键变量 handler事件句柄对象和set数组
1
2
3
4
5
6
7
8
9
10
11
12
// Handler对象
{
del:代理函数
e:事件名称字符串
fn:事件句柄函数
i:该事件注册的事件函数个数
ns
proxy:代理执行函数
sel:选择器字符串
}

// set数组:存储Handler对象

$.trigger()

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
$.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)
})
}

// triggers event handlers on current element just as if an event occurred,
// doesn't trigger an actual event, doesn't bubble
$.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
}
  • 其思路为:

    • 先判断是否支持原生接口dispatchEvent。

      • 如果支持,直接调用。

        1
        2
        3
        4
        * EventTarget.dispatchEvent(event):向一个指定的EventTarget派发一个事件,以合适的顺序触发受影响的EventListeners
        * event:要被派发的event事件;EventTarget是指被初始化的Event.target和决定触发event listeners。
        * 当至少一个该事件的 event handler 调用了Event.preventDefault(),则返回值为false;否则返回true。
        cancelled = !target.dispatchEvent(event)
    • 如果不支持,则调用自己实现的函数triggerHandler()

$.off()

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
// 主要函数remove(),主要api:removeEventListener()
$.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)
})
}
function remove(element, events, fn, selector, capture){
var id = zid(element)
;(events || '').split(/\s/).forEach(function(event){
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))
})
})
}
  • remove()函数中,delete操作符删除句柄对象,并调用removeEventListener移除监听事件。

附录:

event模块源码(注:此为拷贝版本,对应的是本博文的内容,最新版本请到zepto官网获取。)

示例代码:zepto.html