JavaScript事件详解-Zepto的事件实现(二)【新增fastclick阅读笔记】

正文

做者打字速度实在不咋地,源码部分就用图片代替了,都是截图,本文讲解的Zepto版本是1.2.0,在该版本中的event模块与1.1.6基本一致。此文的fastclick理解上在看过博客园各个大神的文章后对我确实有很大的帮助,固然,个人某些观点可能不是很准确甚至有错误,欢迎讨论,白天基本在线。css

zepto的event

能够结合上一篇JavaScript事件详解-原生事件基础(一)综合考虑
源码暂且不表,github里还有中文网站都能下到最新版的zepto。整个event模块不长,274行,咱们能够看到,整个event模块,事件绑定核心就是on和off,还有一个trigger用来触发,类观察者模式,能够先看看汤姆大叔的深刻理解JavaScript系列(32):设计模式之观察者模式,其他皆为实现的处理函数。
首先来个demo:html

$("#btn").on("click",function(event){
  console.log(event);
})

一个简单的click事件监听示例。
根据event模块中对于事件的使用来看:
7
8java

on为开始(add)

6

能够看到,绑定函数有五个参数:ios

  • event:事件类型,能够经过空格的字符串方式添加("click mousedown"),或者事件类型为键,函数为值的方式({click:function(),mousedown:function()})。
  • selector:事件委托的节点选择器,可不传
  • data:事件处理程序中的event.data属性
  • callback:事件处理程序的回调函数
  • one:绑定事件后,只触发一次回调

根据参数,咱们能够很轻易的将on分为几部分(上图所示):css3

  1. 递归序列,处理event为键值对的状况
  2. 简写方式,若是只是简单的事件和回调的话($("#btn").on("click",function(){})),one参数不参与简写形式,有单独的one()方法。
  3. 循环zepto对象,由于这里的$this是zepto.init以后生成的对象,这里对于one和selector进行了autoRemove(只触发一次回调),delegetor(事件代理),而后是add(事件注册)

autoRemove,若是one为true,也就是只想使用一次,那么使用remove,并经过apply,给callback设立event对象;
而delegator中,若是selector是绑定元素的子节点,zepto以event.target为目标元素,判断是否触发节点的父级和传入的selector一致,上下文是遍历以后的节点。而后建立一个该事件对象的副本(createProxy),返回compatible()函数处理的event,固然,最后都会经过add()来进行注册:
10git

首先是zid,zepto里面有个handlers对象,用于存放处理过的事件对象,_zid初始值为1,每次会按照值存入handlers,而且修改event对象中的_zid,每次存入的是一次绑定的全部事件:
12github

由于每次使用$()建立的zepto对象都是新的,用handlers创建队列才能更好的进行管理。
以后就是对于以空白字符形式(/\s/)进行分割的字符串的处理,内部建立了handle,注意其parse方法是内部方法,而不是Date.parse()。chrome

前面也说过,冒泡事件会有反作用,mouseovermouseout,若是只是简单的节点,没有问题,但有了子节点以后。原先监听父节点的事件,会在鼠标移过去时再次触发。这是因为监听的是整个父节点,而移动到子节点时,子节点并无事件,因此向上冒泡所形成的bug,而在DOM3级中,新定义了两个不冒泡的事件:mouseenter,mouseleave,使用这两个事件,能够解决这一问题。而在zepto中,使用了relatedTarget属性,并使用contains判断触发的节点在不属于移出(mouseover),移入(mouseout)时,才执行回调。且对不支持mouseenter和mouseleave事件的状况进行了兼容。设计模式

而后就是调用addEventListener,开始监听,这里没有作IE的兼容。事件句柄随着handler插入handles中,为以后的remove作准备。这里的proxy是对于event的扩充,也是添加了当return false时,调用
preventDefault()和stopPropagation()。api

off移除

9

能够看出来,和on是对应的写法,一样能够分三部分,只不过这里的功能是移除监听而已。

就直接到了remove,这里主要作的就是根据传入的event和selector,用findHandlers进行查找,而后删除handlers中对应的事件,同时调用removeEventListener来移除事件处理程序。

$.Event自定义事件

13

这里使用的是createEvent()和initEvent(),这里的事件类型若是不是specialEvents中定义的MouseEvents,就会变成默认Events(DOM3)。
看下自定义的事件对象:
14

能够发现,由于使用compatible()封装了一下event,因此会有zepto新增的属性,以及咱们传入的props属性。

trigger触发

咱们知道,DOM3级中提供的触发事件的api是dispatchEvent()(低版本IE中是fireEvent()),而zepto这里也是同样:
15

能够看出,对于参数event为对象时进行了处理,意味着能够直接使用trigger建立+触发,支持dispatchEvent的状况下,会直接触发,若是不是DOM节点,则使用triggerHandler()来触发。

triggerHandler触发

16

能够看出,若是节点在以前绑定了其余事件处理程序且使用过stopImmdiatePropagation(),则也不会再触发自定义事件。
demo:
17

$.proxy

这实际上是个独立的函数,在add()函数中使用的proxy,是handler.proxy()函数,与这个无关。这个函数起到的做用很相似extend,只不过它扩展的是上下文(执行环境)。
我却是以为,最关键的就是

fn.apply(fn.apply(context, args ? args.concat(slice.call(arguments)) : arguments))

$.proxy.apply(null,args);

自模拟click事件

zepto实际是为了能在手机上轻量级的使用而创造的,为了使用上的不卡顿,手机上不能用click事件,有延时,缘由很少说了,写个小demo,能够看看pc和手机的点击事件耗时区别:
touch和click demo
18

若是pc端,最好用chrome,没大考虑兼容。用开发者工具也能模拟touch事件。
因此zepto提供了touch模块,我先本身模拟了下tap事件,连着又自定义了一些事件:
touch和tap demo
19

个人思路是,touch事件触发的很快,那么就用touch事件来模拟click,以touchstart和touchend为开始和结束,只考虑了单指的状况,touch事件自己就有:

  • touches:表示当前跟踪的触摸操做的touch对象的数组。
  • targetTouches:特定于事件目标的touch对象的数组。
  • changeTouches:表示自上次触摸以来发生了什么改变的touch对象的数组。
    这三个数组里会包含下列属性:
  • clientX:触摸目标在视口中的x坐标。
  • clientY:触摸目标在视口中的y坐标。
  • identifier:标识触摸的惟一ID。
  • pageX:触摸目标在页面中的x坐标。
  • pageY:触摸目标在页面中的y坐标。
  • screenX:触摸目标在屏幕中的x坐标。
  • screenY:触摸目标在屏幕中的y坐标。
  • target:触摸的DOM节点目标。
    在touchstart中可使用touches,而在touchend里则没有touches数组信息了,用changeTouches来获取触摸结束时的信息。我主要使用了pageX和pageY,判断在250ms之下的状况里,若是手指在一个点上(我设了14px大小的范围,)touchstart加touchend都触发了,则为一次tap事件,仍是用了creatEvent,因此返回的是一个自定义的tap事件
    20

我没有把touch的属性填入自定义的事件里,就一层,因此也没考虑冒不冒泡了,后面能够完善下,tap却是还好,dbltap稍稍耗了点时间。

zepto的touch模块

整个touch模块也很简单,仍是先从入口开始:
21
能够看到zepto新增了这些事件,并作了简写的处理,整个部分最重要的是给document绑定了touch,MSpointer,pointer,MSGestureEvent的触摸事件,不过上面的
22
来看,应该有setTimeout,下面的cancelAll()中也有使用clearTimeout。
先看touch事件,其余的反正是兼容。
zepto定义的局部变量touch中有四个值,x1,x2,y1,y2,应该是用来记录第一个触发的点和第二个触发的点,果真在监听touchmove事件的回调里,使用了两个点来计算偏移,不过这里是将途中全部偏移量都与初始值进行比较,而后汇总。
在touchend中,对于swipe(滑动)和tap(点击)进行了处理,由于deltaX和deltaY须要在30*30的范围内才会被触发,可是它的偏移量是move移动的总和,因此在触发时容错率低,也就是很差点出来,相比较其余操做而言。
在touchcancel的处理中,清除全部延迟操做。
同时其整个操做实际上是绑定在了document上,因此使用时若是有其余的touch事件也绑定在了doucment上,而且取消了冒泡事件,则以后的全部操做都会失效。

touch的点透

专门写了下demo来测试点透问题,点透事件的发生。这里也是以前的click300ms的延时带来的问题,若是最上层始终存在还好,就怕是点击消失的状况,那么若是上层用的是touch事件,下层是a标签,input或者绑定了click事件的节点,则也会被触发,只能说zepto的touch事件还须要咱们本身来扩充和完善。
在经过对javaScript事件的详细学习以后,仍是有不少途径去解决这个问题的,好比:

  1. preventDefault(),来取消touchstart和touchend的默认事件。
  2. 给两层之间加透明的中间层,用于阻止300ms以后的click传递到下层中。
  3. pointer-events:none;该css3属性能够取消节点上的全部click事件。惋惜浏览器支持不良好23
  4. fastclick,老早就想读下fastclick如何实现的,正好阅读一下。

fastclick

使用十分简单,只要FastClick.attach(document.body);就好,在github上也介绍了另两种方法,能够不使用fastClick来快速点击。
24
具体的能够直接去它的github地址看https://github.com/ftlabs/fastclick
我修改了下以前的例子,新增了一个点击事件的例子,能够看出,基本比touchend慢1ms。并且使用了click事件,因此也不存在点透事件。

一样从入口进入,在代码内部,先实例化FastClick(),它有两个参数:layer和options,layer也就是咱们以前传的document.body,
在FastClick()函数中,能够先找到
25

至关于document.body上的节点的click,touch事件的监听都会被FastClick内部的事件给处理掉,若是layer上有onclick事件,一样会被oldOnClick复写。

在pc上不用多说,绑定操做的节点确定先经过对click的监听来处理,
26

这里的trackingClick应该是若是以前页面中的touchend被UI事件阻塞(能够简单模拟下,也就是出现字符被选中的状况),从新置空它。
而在手机上,最早触发的操做一定是touchstart和touchmove,touchend。
27

这里的targetElement很重要,实际上是上面提过的event的target,实际触摸的节点目标,这个属性会在接下来的move,end中用来判断是否中间换过节点,并在sendClick中用来真实的触发事件
28

返回touchstart中,兼容先不看,这里对于初始节点也作了存储,touchStartX,touchStartY
而接下来的对于double-tap的处理,其实须要看需求,若是是默认值,那么200ms内的第二次点击会禁止触发其默认事件event.preventDefault(),在手机上就没法触发双击事件。

而后就是touchmove,这里却是很短,只是对于targetElement作了个判断,是不是同一个触摸节点。
29

在touchend中,一样略过兼容不看,不过我却是又学到了一个原生api,document.elementFromPoint,传入x,y来找到节点,只是认识了一下,实不实用不表。
30

其实一共有三种方式来操做fastclick内部自定义的事件,不过当targetTagName=label时,这里focus了一下,把这里的触发放到了needsClick中而已,而在这两块中,能够知道,若是给className中加入needsFocusneedsClick,则会中断fastclick,使用原生事件:

  • needsFocus:第一个判断事件从touchstart到touchend是否超过100ms,则将targetElement置空,其实就是回归原来的方法,取消fastclick的事件。而后就是focus事件
    31
    这里使用了setSelectionRange(length,length),先选中文本。
    32
    有兴趣的能够去MDN看,不过我测试了一下,上面的例子中有个input,其中确实会有http://www.cnblogs.com/vajoy/p/5522114.html#!/follow提到的问题,在快速点击时光标会直接移到最后面,而上面传入的参数都是length,天然就跑到了最后面。
    再经过sendClick来触发,就会有问题,并且在触发完自定义click以后,
    33
    取消了咱们点击时带的focus操做,这里我先注释了一下代码,果真,光标先定到末尾,再定到正确位置,虽然上述博主的方法暂缓此问题,但在有些浏览器中仍然存在这一问题,而且实际上仍然是光标定到末尾,再定到正确位置,不如直接不用模拟的focus,做者本身的注释里说的是ios7中有问题,但focus中用的是deviceISIOS,而不是deviceIsIOSWithBadTarget,改这里也能解决问题。

  • needsClick:这里首先preventDefault,再使用sendClick,上面稍微介绍了下,这里模拟的都是MouseEvents事件,使用targetElement来触发。

那么为什么fastclick不穿透呢,我天然仍是找代码中哪里用了preventDefaultstopImmediatePropagation,在onMouse中,仍是防止快速点击而加的阻止操做,实际上阻止点透的仍然是needsClick中的event.preventDefault(),去掉以后,页面点击仍然会点透。

比较

知道了仍是preventDefault()起了做用,咱们再回头看touch模块,对比一下fastclick,其实二者的主体层都是差很少的,document和document.body实际在大多数状况下,可点击域都是同样,也就是说二者都绑定到了最外层上(近似),但fastclick提供了layer和options,意味着能够规避风险,而zepto的touch模块则直接绑定到了document之上,至少在使用上,并无fastclick方便,不过其定义的各类touch动做,颇有意义,在上面说过,在zepto解决点透,能够:

$("#btn").tap(function(){
  // do something
}).on('touchend',function(e){
  e.preventDefault();
});

在读源码时,layer绑定的部分,上面有图,确实会误导。由于实际上onClick是能够不走的,之因此在手机上触发的仍然是click事件,是由于在sendClick里直接使用dispatchEvent()触发了click,因此才会从fastclick定义的this.onClick()中走,其实这一部分能够和zepto同样放入touchend模块里面。
这么看,两个代码的核心其实大体相同,所不一样的是fastclick中还加入的tap事件的focus方法,若是在zepto的touch中直接加入preventDefault(),则input没法得到焦点,因此能够引入focus事件,从而解决问题
34
35
可能会有兼容性的问题,对于blur()和focus()写完jQuery以后能够加入兼容性的写法。
如今就不会点透了。
还能够把touchstart,touchend操做放到具体的节点上,而后在节点中进行处理,不过这样会改动的比较大。zepto中有个方法$.proxy,放在这里有奇效,上面介绍过。

其余

至于说delegate,undelegate,live,die这些代理事件,还有bind,unbind等绑定操做,其实都是on在起做用,不细说。中间去看了fastclick,以及其余事情,其实星期一就写完了,仍是基础薄弱啊。
以后是看看jQuery的事件操做,压力山大。

相关文章
相关标签/搜索