你有想过没,当你监听某个DOM元素的一个事件时,其事件处理函数是如何和该DOM元素关联起来的呢:
1 var wp=document.getElementById(‘wrapper’); 2 wp.addEventListener(‘click’,function(){ 3 // event handler 4 });
你又想过没,当你监听某个对象上的自定义事件时,其事件处理函数是如何和该对象关联起来的, 事件是如何被触发的,这背后的库,又做了什么呢:
1 var obj={} 2 $(obj).on(‘fire’,function(){ 3 // event handler 4 })
带着这些问题,我们以zepto库为原型,从代码实现的角度来一窥究竟:
首先,我们构造一个mini库,以$记之吧.为简单起见,它只做两件事:id选择器,each方法:
1 <script> 2 $ = (function () { 3 function typestr(o) { 4 var s = Object.prototype.toString.call(o); 5 if (s == "[object Object]") return “object”; 6 else if (s == "[object String]") return “string”; 7 else; 8 } 9 var _$ = function (node) { 10 if (typestr(node) == “string”) node = document.getElementById(node.slice(1)); 11 var rev = [node]; 12 rev.__proto__ = _$.fn; 13 14 return rev; 15 } 16 _$.fn = { 17 each: function (callback) { 18 for (var i = 0; i < this.length; i++) { 19 callback(this[i], i); 20 } 21 } 22 }; 23 return _$; 24 })();</script>
下面是核心部分,我们先完成准备工作,声明一个计数器和一个对象来维护事件,然后给出基本骨架:
1 <script> 2 (function($){ 3 var handlers = {}; 4 var _zid = 1; 5 function zid(element) { 6 return element._zid || (element._zid = _zid++); 7 } 8 function add(element,event,callback){ 9 // 内部添加事件 10 } 11 function remove(element,event,callback){ 12 //内部删除事件 13 } 14 $.fn.on=function(event,callback,one){ 15 //对外公开监听事件方法 16 //add 17 } 18 $.fn.off=function(event,callback){ 19 //对外公开移除事件的方法 20 //remove 21 } 22 $.fn.one=function(event,callback){ 23 } 24 $.fn.trigger=function(event){ 25 //对外公开触发事件的方法 26 } 27 })($); 28 </script>
add方法:
1 function add(element, event, callback) { 2 var id = zid(element), set = (handlers[id] || (handlers[id] = [])), handler = {}; 3 handler.en = event; 4 handler.ev = callback; 5 handler.i=set.length; 6 set.push(handler); 7 if (‘addEventListener‘ in element) { 8 element.addEventListener(handler.en,callback, false); 9 } 10 }
如果某元素注册过事件,就通过它的_zid值去handlers中找到事件队列,将新的事件对象添加进队列;如果该元素没注册过事件,则在handlers中开辟一个以_zid关联的新队列,再将事件对象添加进队列. 事件队列的长度正好是新添加事件对象在事件队列中的位置,记录该位置,可方便后面从事件队列中删除该事件对象.
这里的’元素’指的是对象,因为DOM元素上的事件是用addEventListener方法来通知浏览器,让浏览器来为我们来作类似的事情.
remove方法:
1 function remove(element,event,callback){ 2 var id=zid(element); 3 var set=handlers[id]||[]; 4 set.forEach(function(handler){ 5 if(handler.en==event){ 6 delete set[handler.i]; 7 if("removeEventListener" in element){ 8 element.removeEventListener(event,callback,false); 9 } 10 } 11 }); 12 }
和add方法作相反的事情,对象和DOM元素也是分别对待:
两个核心方法讲完,看看对外公开的几个方法:
on/one/off方法:
1 $.fn.on = function (event, callback,one) { 2 var cbx=callback; 3 this.each(function (elem, index) { 4 if(one){ 5 cbx=function(){ 6 callback(); 7 remove(elem,event,cbx); 8 9 }; 10 } 11 add(elem, event,cbx); 12 }); 13 14 }; 15 $.fn.one=function(event,callback){ 16 this.on(event,callback,1); 17 }; 18 19 $.fn.off=function(event,callback){ 20 this.each(function (elem, index) { 21 remove(elem, event, callback); 22 }); 23 };
在on方法里给one方法预留了一个判断 ,在执行callback一次后,就remove掉该事件,该事件就不会再次被触发;
trigger方法:
1 $.fn.trigger = function (event, args) { 2 var elem = this[0],set = handlers[zid(element)],len=set.length,handler; 3 for (var i = 0; i <len; i++) { //forEach 4 handler=set[i]; 5 if(handler.en==event){ 6 if(‘dispatchEvent‘ in elem) elem.dispatchEvent(event) 7 handler.ev.call(this,args); 8 } 9 } 10 };
通过计数器去查看元素的_zid值,然后去handlers中查找事件队列,循环事件队列,执行相应处理函数,如果是DOM元素,用dispatchEvent方法来告知浏览器触发事件.
就上面看来,大部分代码是用来解决如何通过维护事件队列来监听,移除,触发一个对象上的事件.对于DOM元素上的事件来说,我们只是通过addEventListener方法告知浏览器,要注册事件.通过removeEventListener方法告知浏览器,要移除事件了,但浏览器是如何维护它的事件队列的,对于我们来讲,是透明的.另外事件的触发也靠浏览器的自身的机制去完成的,例如,浏览器如果检测到一个DOM元素被单击了,它会去触发click事件以执行相应的处理函数.也就是说,浏览器形为和事件之间是有对应或契约关系的.我们常见的DOM元素上面一些默认的事件,都是以这种方式来处理的.
上面的代码为了做到尽可能的简单,很多地方做了简化,这里要提一点的是,自定义事件的用法:
1 // Create the event. 2 var event = document.createEvent(‘Event‘); 3 // Define that the event name is ‘build‘. 4 event.initEvent(‘build‘, true, true); 5 // Listen for the event. 6 document.addEventListener(‘build‘, function (e) { 7 // e.target matches document from above 8 }, false); 9 // target can be any Element or other EventTarget. 10 document.dispatchEvent(event);
相关内容参考: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
移动端click事件问题
因为移动端的单击事件会延时,zepto的tap事件据说又很坑爹,所以,果断决定来模拟一个自己的tap事件:
1 (function (root, $) { 2 var x, y, target, startTime; 3 root = $(root); 4 root.bind(‘touchstart‘, function (e) { 5 target = $(event.target); 6 var touch = event.changedTouches[0]; 7 x = touch.pageX; 8 y = touch.pageY; 9 startTime = new Date().getTime(); 10 }).bind(‘touchend‘, function (e) { 11 var touch = event.changedTouches[0]; 12 var tx = (new Date().getTime() - startTime); 13 var cx = touch.pageX; 14 var cy = touch.pageY; 15 if (Math.abs(cx - x) <= 10 && Math.abs(cy - y) <= 10 && tx <= 500) { 16 var ev = $.Event(‘tap‘); 17 target.triggerHandler(ev); 18 } 19 }); 20 })(document,zepto)
这里$.Event的内部实现其实就用到了上面提到的自定义事件.我们这里已经定义好了tap事件的触发时机,只待事件注册了.
改用tap注册事件,事件执行确实是快了,但是,它却带来了新的问题:
场景 当我们在一个弹出层的关闭按钮上面用tap注册了一个事件,功能是单击后,弹出层消失.
效果: 确实能让对弹出层消失,但是如果关闭按钮下方刚好有个文本框,或是有一个上面已经注册了其他事件的DOM元素,你会发现不被期望的事情发生了.要么是键盘弹出来了,要么是触发了DOM元素上的事件,页面跳转了.更有甚者,是导致页面跳转,触发了下个页面上元素的事件.
执行得太快,也是个错么?
这个问题一度的解决方案是,定义一个白色的透明层,执行tap事件时,立马把整个屏幕罩起来,0.8s后,移除遮罩:
1 /*#ng{position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0.0;z-index:1999;}*/ 2 var ng=$(‘#ng’); 3 ng.show(); 4 setTimeout(function(){ng.hide();},800)
后来受叶小钗同学一文的启发,还是用click事件:
1 if (Math.abs(cx - x) <= 10 && Math.abs(cy - y) <= 10 && tx <= 500) { 2 var ev = $.Event(‘click.me‘); 3 target.triggerHandler(ev); 4 }
为了避免click执行两次,在自定义的click事件里,我给加了个.me的别名,用intel XDK找了几款机型测试了下,暂时没发现什么问题,有兴趣的同学可以试试!
Event in Zepto