Event in Zepto

你有想过没,当你监听某个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>
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    //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>
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元素,你会发现不期望的事情发生了:

   1:  系统键盘弹出来了;

   2:  触发了DOM元素上的事件,页面跳转了;

   3:  导致页面跳转,触发了下个页面元素上的事件;

执行得太快,也是个错么?

这个问题一度的解决方案是定义一个白色的透明层,执行tap事件时,立马把整个屏幕罩起来,0.8s后,移除遮罩:

1 /*#ng{position:fixed;top:0;left:0;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找了几款机型测试了下,暂时没发现什么问题,有兴趣的同学可以试试!

原文地址:https://www.cnblogs.com/stenson/p/3924265.html