Zepto事件模块源码分析
一、保存事件数据的handlers
我们知道js原生api中要移除事件,需要传入绑定时的回调函数。而Zepto则可以不传入回调函数,直接移除对应类型的所有事件。原因就在于Zepto在绑定事件时,会把相关的数据都保存到handlers
对象中,因此就可以在这个变量中查找对应事件的回调函数,来移除事件。
handlers
对象的数据格式如下:
{ 1: [ // handlers的值为DOM元素的_zid { del: function() {}, // 实现事件代理的函数 e: "click", // 事件名称 fn: function() {}, // 用户传入的回调函数 i: 0, // 该对象在数组里的下标 ns: "", // 事件的命名空间,只用使用$.fn.triggerHandler时可用,$.fn.trigger不能使用。 proxy: function(e) {}, // 真正绑定事件时的回调函数,里面判断调用del或者fn sel: undefined // 要进行事件代理时传入的selector } ] }
二、绑定事件
主要流程图
流程说明
处理参数实现函数重载
实现函数重载的重点就是判断参数的类型,处理参数:
// 处理参数,实现函数重载 if (!isString(selector) && !isFunction(callback) && callback !== false) // 没有传入selector参数 callback = data, data = selector, selector = undefined if (callback === undefined || data === false) // 没有传入data参数 callback = data, data = undefined if (callback === false) callback = returnFalse // 回调函数传入的是false,使用returnFalse函数代替
构建事件代理函数
事件代理函数的重点为:在触发元素和e.target
之间找到和参数selector相匹配的元素,生成事件对象,触发回调函数:
// 如果有传入selector参数,构建代理函数 if (selector) delegator = function(e){ var evt, match = $(e.target).closest(selector, element).get(0) // match为e.target到触发元素范围内和selector相匹配的元素 if (match && match !== element) { // 如果有相匹配的元素,改写事件对象,触发回调函数 evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) } }
保存事件的相关信息
保存事件的相关信息就是根据参数组成handler对象,保存到上面提过的handlers
对象中。需要注意的是Zepto是通过DOM对象中添加一个_zid来连接DOM对象和对应的handler对象。
使用zid函数来赋值和获取_zid:
function zid(element) { return element._zid || (element._zid = _zid++) }
通过一个_zid而不是通过DOM对象的引用来连接handler是因为:防止移除掉DOM元素后,handlers对象还保存着对这个DOM元素的引用。通过使用_zid就可以防止这种情况发生,避免了内存泄漏。
构建真正的回调函数proxy
Zepto对事件对象进行了扩展,例如添加isImmediatePropagationStopped
函数等,所以就要构建proxy函数来进行一层代理,改变触发时的事件对象。此外,如果用户需要进行事件代理,proxy函数执行时就会调用上面构建好的代理函数,否则调用用户传进来的回调函数。因此使用addEventListener
所传入的真正回调函数就是proxy函数。
focus和blur事件的冒泡
focus和blur事件本身是不冒泡的,如果需要对这两个事件进行事件代理,就要运用一些小技巧。首先,如果浏览器支持focusin和focusout,就使用这两个可以冒泡事件来代替。如果浏览器不支持focusion和focusout,就利用focus和blur捕获不冒泡的特性,传入addEventListener
中的第三个参数设置true,以此来进行事件代理。
三、取消绑定事件
取消绑定是比较简单的,由于事件绑定时都把相应的数据都保存到了handlers
对象上,所以只要根据参数在这个对象里寻找对应的回调函数,使用removeEventListener
取消绑定就可以了。
四、触发事件
触发事件也比较简单,通过document.createEvent
来创建事件对象,然后调用dispatchEvent
来触发事件。这里有点小优化就是focus和blur事件的触发直接调用focus()和blur()。
创建事件的代码:
$.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) }
上面有点要注意的就是当创建鼠标相关的事件时要在document.createEvent
的第一个参数中传入’MouseEvents‘,以提供更多的事件属性。鼠标相关的事件指的是:click、mousedown、mouseup和mousemove。
五、其他
源码大概只有300多行,中其实还有很多值得我们学习的地方,所以大家大可以花点时间阅读一下。
六、event模块源代码
// Zepto.js // (c) 2010-2016 Thomas Fuchs // Zepto.js may be freely distributed under the MIT license. ;(function($){ /* _zid :用来生成标示元素和回调函数的id,每标示一个就+1 slice :Array.prototype.slice handlers :保存着所有的handler,结构如下,handlers对象的值为对应元素的_zid,值为对象数组 { 1: [ // handlers的值为DOM元素的_zid { del: function() {}, // 实现事件代理的函数 e: "click", // 事件名称 fn: function() {}, // 用户传入的回调函数 i: 0, // 该对象在数组里的下标 ns: "", // 事件的命名空间,只用使用$.fn.triggerHandler时可用,$.fn.trigger不能使用。 proxy: function(e) {}, // 真正绑定事件时的回调函数,里面判断调用del或者fn sel: undefined // 要进行事件代理时传入的selector } ] } specialEvents :生成一个模拟事件时,click、mousedown、mouseup和mousemove时使用'MouseEvents'参数 focusinSupported:是否支持focusin的Boolean值 focus :浏览器支持focusin和focusout时,focus和blur就使用focusin和focusout来代替 hover :mouseenter和mouseout用mouseover和mouseout实现 */ var _zid = 1, undefined, slice = Array.prototype.slice, isFunction = $.isFunction, isString = function(obj){ return typeof obj == 'string' }, handlers = {}, specialEvents={}, focusinSupported = 'onfocusin' in window, focus = { focus: 'focusin', blur: 'focusout' }, hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' } specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents' // 获取元素或者函数的_zid,没有的话就生成一个。赋值给元素或者函数的_zid属性 function zid(element) { return element._zid || (element._zid = _zid++) } // 根据给定的参数在handlers变量中寻找对应的handler function findHandlers(element, event, fn, selector) { event = parse(event) // 解析event参数,分离出事件名和ns if (event.ns) var matcher = matcherFor(event.ns) // // 取出所有属于element的handler,并且根据event、fn和selector参数进行筛选 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)) // 回调函数不同的过滤掉(通过_zid属性判断是否同一个函数) && (!selector || handler.sel == selector) // selector不同的过滤掉 }) } // 解析event参数: "click.abc" -> {e: "click", ns: "abc"} function parse(event) { var parts = ('' + event).split('.') return {e: parts[0], ns: parts.slice(1).sort().join(' ')} } // 生成匹配的namespace表达式:'abc def' -> /(?:^| )abc .* ?def(?: |$)/ function matcherFor(ns) { return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)') } // 获取绑定事件时,指定是否冒泡或捕获阶段的Boolean值 // 需要事件代理和浏览器不支持focusin并且是focus和blur事件时返回ture,即绑定在捕获阶段 // (因为focus和blur不会冒泡,但是会捕获) function eventCapture(handler, captureSetting) { return handler.del && (!focusinSupported && (handler.e in focus)) || !!captureSetting } // 返回真正绑定的事件名,例如要绑定mouseenter事件,其实真正绑定的是mouseover事件来模拟mouseenter事件。 function realEvent(type) { return hover[type] || (focusinSupported && focus[type]) || type } /** * 添加事件的实际方法 * @param {Object} element DOM元素 * @param {String} events 事件字符串 * @param {Function} fn 回调函数 * @param {All} data 绑定事件时传入的data,可以是各种类型 * @param {String} selector 被代理元素的css选择器 * @param {[type]} delegator 进行事件代理的函数 * @param {[type]} 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){ // 如果事件名为ready,直接调用$.fn.ready方法 if (event == 'ready') return $(document).ready(fn) // 构建handler var handler = parse(event) handler.fn = fn handler.sel = selector // emulate mouseenter, mouseleave // mouseenter、mouseleave通过mouseover、mouseout来模拟。 if (handler.e in hover) fn = function(e){ // relatedTarget为相关元素,只有mouseover和mouseout事件才有 // 对mouseover事件而言,相关元素就是那个失去光标的元素。对mouseout事件而言,相关元素则是获得光标的元素。 var related = e.relatedTarget // 只当鼠标从元素外部移到元素内部才触发mouseenter,只当鼠标从元素内部移到元素外部才出发mouseleave。 if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments) } handler.del = delegator var callback = delegator || fn // 需要进行事件代理时,调用的是封装了fn的delegator函数 // 真正绑定事件时的回调函数 // 通过该函数改写事件对象,为事件对象添加一些方法和属性,然后调用用户传进来的回调函数。并且使用户的回调函数返回false时禁止默认行为和禁止冒泡 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 // 把handler在set中的下标赋值给handler.i set.push(handler) // 把handler保存起来 // 最后绑定事件 if ('addEventListener' in element) element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) }) } // 删除handler 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)) }) }) } $.event = { add: add, remove: remove } $.proxy = function(fn, context) { var args = (2 in arguments) && slice.call(arguments, 2) // 第三个以及之后的参数 // 参数fn为函数 if (isFunction(fn)) { var proxyFn = function(){ return fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments) } proxyFn._zid = zid(fn) return proxyFn // 第一个参数为对象的情况:$.proxy(context, "fnName" ) } 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") } } $.fn.bind = function(event, data, callback){ return this.on(event, data, callback) } $.fn.unbind = function(event, callback){ return this.off(event, callback) } $.fn.one = function(event, selector, data, callback){ return this.on(event, selector, data, callback, 1) } var returnTrue = function(){return true}, // 在compatible里有用到 returnFalse = function(){return false}, // 在compatible里有用到 // 构建事件对象时所不要的几个属性:returnValue、layerX和layerY(还有以大写字母开头的属性?) ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/, // 事件对象需要添加的三个方法名 eventMethods = { preventDefault: 'isDefaultPrevented', stopImmediatePropagation: 'isImmediatePropagationStopped', stopPropagation: 'isPropagationStopped' } // 添加eventMethods里面的三个方法:isDefaultPrevented、isDefaultPrevented和isPropagationStopped 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 }) // 设置isDefaultPrevented默认指向的函数 if (source.defaultPrevented !== undefined ? source.defaultPrevented : // 如果有defaultPrevented属性,就根据defaultPrevented的值来判断 'returnValue' in source ? source.returnValue === false : // returnValue属性只在beforeunload事件中有用。而且用途是设置弹出框的文案,所以这行代码应该有问题,可以去掉 source.getPreventDefault && source.getPreventDefault()) // getPreventDefault和defaultPrevented属性类似,不过是非标准的。为了兼容没有defaultPrevented参数的浏览器。 event.isDefaultPrevented = returnTrue } return event } // 构建事件代理中的事件对象 function createProxy(event) { var key, proxy = { originalEvent: event } // 新的事件对象有个originalEvent属性指向原对象 // 将原生事件对象的属性复制给新对象,除了returnValue、layerX、layerY和值为undefined的属性 // returnValue属性为beforeunload事件独有 for (key in event) if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key] // 添加eventMethods里面的几个方法,并返回新的事件对象 return compatible(proxy, event) } $.fn.delegate = function(selector, event, callback){ return this.on(event, selector, callback) } $.fn.undelegate = function(selector, event, callback){ return this.off(event, selector, callback) } $.fn.live = function(event, callback){ // 通过document.body元素代理事件 $(document.body).delegate(this.selector, event, callback) return this } $.fn.die = function(event, callback){ // 移除掉document.body元素的相关代理事件 $(document.body).undelegate(this.selector, event, callback) return this } // 只负责处理参数,绑定事件交由add函数处理 $.fn.on = function(event, selector, data, callback, one){ var autoRemove, delegator, $this = this // event参数为对象,回调自身进行批量绑定事件 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) // 没有传入selector参数 callback = data, data = selector, selector = undefined if (callback === undefined || data === false) // 没有传入data参数 callback = data, data = undefined if (callback === false) callback = returnFalse // 回调函数传入的是false,使用returnFalse函数代替 // 给每一个Z对象里面的元素绑定事件 return $this.each(function(_, element){ // 如果有传入one参数,使用下面的autoRemove函数代替callback。执行一次后会自动移除该事件的绑定 if (one) autoRemove = function(e){ remove(element, e.type, callback) return callback.apply(this, arguments) } // 如果有传入selector参数,构建代理函数 if (selector) delegator = function(e){ var evt, match = $(e.target).closest(selector, element).get(0) // match为e.target到触发元素范围内和selector相匹配的元素 if (match && match !== element) { // 如果有相匹配的元素,改写事件对象,触发回调函数 evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element}) return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) } } // 通过add函数来绑定事件 add(element, event, callback, data, selector, delegator || autoRemove) }) } $.fn.off = function(event, selector, callback){ var $this = this // event参数为对象,回调自身进行批量取消绑定事件 if (event && !isString(event)) { $.each(event, function(type, fn){ $this.off(type, selector, fn) }) return $this } // 处理参数,实现函数重载 if (!isString(selector) && !isFunction(callback) && callback !== false) // 没有传入selector参数 callback = selector, selector = undefined if (callback === false) callback = returnFalse // 回调函数传入的是false,使用returnFalse函数代替 return $this.each(function(){ remove(this, event, callback, selector) }) } $.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 // 通过直接调用focus()和blur()方法来触发对应事件,这算是对触发事件方法的一个优化 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 } // shortcut methods for `.bind(event, fn)` for each event type // 绑定和触发事件的快捷方式 ;('focusin focusout focus blur load resize scroll unload click dblclick '+ 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+ 'change select keydown keypress keyup error').split(' ').forEach(function(event) { $.fn[event] = function(callback) { return (0 in arguments) ? this.bind(event, callback) : this.trigger(event) } }) // 生成一个模拟事件,如果是鼠标相关事件,document.createEvent传入的第一个参数为'MouseEvents',以提供更多的参数 $.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) } })(Zepto)
来源:https://www.cnblogs.com/oadaM92/p/5303137.html