首页 > 代码库 > Event in Zepto

Event in Zepto

你有想过没,当你监听某个DOM元素的一个事件时,其事件处理函数是如何和该DOM元素关联起来的呢:

1 var wp=document.getElementById(‘wrapper’);2 wp.addEventListener(‘click’,function(){3       // event handler4 });

你又想过没,当你监听某个对象上的自定义事件时,其事件处理函数是如何和该对象关联起来的, 事件是如何被触发的,这背后的库,又做了什么呢:

1 var  obj={}2 $(obj).on(‘fire’,function(){3     // event handler4 })

带着这些问题,我们以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>
View Code

下面是核心部分,我们先完成准备工作,声明一个计数器和一个对象来维护事件,然后给出基本骨架:

 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    //add17 }18 $.fn.off=function(event,callback){19    //对外公开移除事件的方法20    //remove21 }22 $.fn.one=function(event,callback){23 }24 $.fn.trigger=function(event){25      //对外公开触发事件的方法26 }27 })($);28 </script>
View Code

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)
View Code

这里$.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找了几款机型测试了下,暂时没发现什么问题,有兴趣的同学可以试试!