能够深入理解zepto对事件的处理,那么整个JS的事件处理就应该差不多合格了,事件处理是JS语言的一个难点。
1. 首先来看$.event函数
JS中有很多事件,都是已经定义好了,我们直接调用就可以,例如熟悉的click事件,直接对dom绑定一个事件,点击该dom就能触发这个事件,但是有这样的场景:我点击一个dom,重新打开一个页面。按照常规,可以通过window.open来执行,也可以模拟一个连接,在这个链接上绑定click,之后触发这个click事件。代码如下:
var a = document.createElement("a"); a.setAttribute("href", url); a.setAttribute("target", "_blank"); a.setAttribute("id", "openwin"); document.body.appendChild(a); //模拟点击事件 var m = document.createEvent("MouseEvents"); //FF的处理 m.initEvent("click", true, true); a.dispatchEvent(m);
在zepto.js的ajax源码中,有很多注册事件,这些事件都是通过下来代码来完成的。
function triggerAndReturn(context, eventName, data) { //步骤1:注册事件 var event = $.Event(eventName) //步骤2:分发事件 $(context).trigger(event, data) return !event.defaultPrevented }
在注册和分发事件中,有三个步骤,对于与三个函数:
document.createEvent() //新建
event.initEvent() //初始化
this.dispatchEvent() //元素触发事件
来看看$.event源码:
$.Event = function(type, props) { if (!isString(type)) props = type, type = props.type //click,mousedown,mouseup,mousemove的事件是MouseEvents,其他的是Events。 var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true //event.initEvent(eventType,canBubble,cancelable) //eventType 字符串值。事件的类型。 //canBubble 事件是否起泡。 //cancelable 是否可以用 preventDefault() 方法取消事件。把bubbles从props中过滤出来,单独处理。 if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name]) event.initEvent(type, bubbles, true) return compatible(event) } /* stopImmediatePropagation方法作用在当前节点以及事件链上的所有后续节点上。 目的是在执行完当前事件处理程序之后,停止当前节点以及所有后续节点的事件处理程序的运行。 stopPropagation方法作用在后续节点上,目的在执行完绑定到当前元素上的所有事件处理程序之后,停止执行所有后续节点的事件处理程序 */ eventMethods = { preventDefault: 'isDefaultPrevented', stopImmediatePropagation: 'isImmediatePropagationStopped', stopPropagation: 'isPropagationStopped' } function compatible(event, source) { if (source || !event.isDefaultPrevented) { source || (source = event) /* 给event注册6个函数,默认[isDefaultPrevented,isImmediatePropagationStopped,isPropagationStopped] = false, 执行preventDefault之后,对应的preventDefault = {return true;} */ $.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 : 'returnValue' in source ? source.returnValue === false : source.getPreventDefault && source.getPreventDefault()) event.isDefaultPrevented = returnTrue } return event }
再来看看$.trigger事件:
function fix(event) { if (!('defaultPrevented' in event)) { event.defaultPrevented = false var prevent = event.preventDefault //通过preventDefault取消事件的触发。 event.preventDefault = function() { this.defaultPrevented = true prevent.call(this) } } } $.fn.trigger = function(event, data){ if (typeof event == 'string') event = $.Event(event) //添加preventDefaulted成员和重载preventDefault事件。 fix(event) event.data = data return this.each(function(){ //是dom节点都会有dispatchEvent事件,不是dom,如果有dispatchEvent事件也会执行。 if('dispatchEvent' in this) this.dispatchEvent(event) }) }
还有一个triggerHandler事件,它与trigger有四个不同点:
- 它不会引起事件(比如表单提交)的默认行为
- trigger() 会操作 jQuery 对象匹配的所有元素,而 .triggerHandler() 只影响第一个匹配元素。
- 由 .triggerHandler() 创建的事件不会在 DOM 树中冒泡;如果目标元素不直接处理它们,则不会发生任何事情。
- 该方法的返回的是事件处理函数的返回值,而不是具有可链性的 jQuery 对象。此外,如果没有处理程序被触发,则这个方法返回 undefined。
这个后面我们再讨论。
2.事件对象
用JS原生态的绑定事件很easy,而zepto对绑定事件进行重重封装,最明显的莫过于event对象,常规绑定事件。
//每个element都有一个_zid来判断该element上已经绑定了几个事件。 var id = zid(element), set = (handlers[id] || (handlers[id] = [])) eachEvent(events, fn, function(event, fn){ //如果没有事件委托,add只有element, events, fn三个参数 //element: 元素节点 events=["click","mouseup"...] //fn:函数名或者匿名函数。 var delegate = getDelegate && getDelegate(fn, event), callback = delegate || fn var proxyfn = function (event) { var result = callback.apply(element, [event].concat(event.data)) //callback返回为false,阻止默认事件。 if (result === false) event.preventDefault() return result } /* event = { e: 事件名称 ns: 事件命名空间的对象 data: 参数 } */ var handler = $.extend(parse(event), { fn: fn, proxy: proxyfn, sel: selector, del: delegate, i: set.length }) set.push(handler) element.addEventListener(handler.e, proxyfn, capture) }) }
委托绑定事件:
$.fn.delegate = function(selector, event, callback){ //委托事件不需要冒泡到父节点,只针对特定元素。 var capture = false if(event == 'blur' || event == 'focus'){ if($.iswebkit) event = event == 'blur' ? 'focusout' : event == 'focus' ? 'focusin' : event else capture = true } return this.each(function(i, element){ add(element, event, callback, selector, function(fn){ return function(e){ var evt, match = $(e.target).closest(selector, element).get(0) //匹配到特定的元素 if (match) { evt = $.extend(createProxy(e), { currentTarget: match, liveFired: element }) return fn.apply(match, [evt].concat([].slice.call(arguments, 1))) } } }, capture) }) }
还有一种绑定事件,只绑定一次,
$.fn.one = function(event, callback){ return this.each(function(i, element){ //没有子元素选择参数 add(this, event, callback, null, function(fn, type){ return function(){ var result = fn.apply(element, arguments) //触发之后,删除该事件。 remove(element, type, fn) return result } }) })