正文

作者打字速度實在不咋地,源碼部分就用圖片代替了,都是截圖,本文講解的Zepto版本是1.2.0,在該版本中的event模塊與1.1.6基本一致。此文的fastclick理解上在看過博客園各個大神的文章后對我確實有很大的幫助,當然,我的某些觀點可能不是很準確甚至有錯誤,歡迎討論,白天基本在線。

zepto的event

可以結合上一篇JavaScript事件詳解-原生事件基礎(一)綜合考慮
源碼暫且不表,github里還有中文網(wǎng)站都能下到最新版的zepto。整個event模塊不長,274行,我們可以看到,整個event模塊,事件綁定核心就是on和off,還有一個trigger用來觸發(fā),類觀察者模式,可以先看看湯姆大叔的深入理解JavaScript系列(32):設計模式之觀察者模式,其余皆為實現(xiàn)的處理函數(shù)。
首先來個demo:

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

一個簡單的click事件監(jiān)聽示例。
根據(jù)event模塊中對于事件的使用來看:
7
8

on為開始(add)

6

可以看到,綁定函數(shù)有五個參數(shù):

  • event:事件類型,可以通過空格的字符串方式添加("click mousedown"),或者事件類型為鍵,函數(shù)為值的方式({click:function(),mousedown:function()})。
  • selector:事件委托的節(jié)點選擇器,可不傳
  • data:事件處理程序中的event.data屬性
  • callback:事件處理程序的回調(diào)函數(shù)
  • one:綁定事件后,只觸發(fā)一次回調(diào)

根據(jù)參數(shù),我們可以很輕易的將on分為幾部分(上圖所示):

  1. 遞歸序列,處理event為鍵值對的情況
  2. 簡寫方式,如果只是簡單的事件和回調(diào)的話($("#btn").on("click",function(){})),one參數(shù)不參與簡寫形式,有單獨的one()方法。
  3. 循環(huán)zepto對象,因為這里的$this是zepto.init之后生成的對象,這里對于one和selector進行了autoRemove(只觸發(fā)一次回調(diào)),delegetor(事件代理),然后是add(事件注冊)

autoRemove,如果one為true,也就是只想使用一次,那么使用remove,并通過apply,給callback設立event對象;
而delegator中,如果selector是綁定元素的子節(jié)點,zepto以event.target為目標元素,判斷是否觸發(fā)節(jié)點的父級和傳入的selector一致,上下文是遍歷之后的節(jié)點。然后創(chuàng)建一個該事件對象的副本(createProxy),返回compatible()函數(shù)處理的event,當然,最后都會通過add()來進行注冊:
10

首先是zid,zepto里面有個handlers對象,用于存放處理過的事件對象,_zid初始值為1,每次會按照值存入handlers,并且修改event對象中的_zid,每次存入的是一次綁定的所有事件:
12

因為每次使用$()創(chuàng)建的zepto對象都是新的,用handlers建立隊列才能更好的進行管理。
之后就是對于以空白字符形式(/\s/)進行分割的字符串的處理,內(nèi)部創(chuàng)建了handle,注意其parse方法是內(nèi)部方法,而不是Date.parse()。

前面也說過,冒泡事件會有副作用,mouseovermouseout,如果只是簡單的節(jié)點,沒有問題,但有了子節(jié)點之后。原先監(jiān)聽父節(jié)點的事件,會在鼠標移過去時再次觸發(fā)。這是由于監(jiān)聽的是整個父節(jié)點,而移動到子節(jié)點時,子節(jié)點并沒有事件,所以向上冒泡所造成的bug,而在DOM3級中,新定義了兩個不冒泡的事件:mouseenter,mouseleave,使用這兩個事件,可以解決這一問題。而在zepto中,使用了relatedTarget屬性,并使用contains判斷觸發(fā)的節(jié)點在不屬于移出(mouseover),移入(mouseout)時,才執(zhí)行回調(diào)。且對不支持mouseenter和mouseleave事件的情況進行了兼容。

然后就是調(diào)用addEventListener,開始監(jiān)聽,這里沒有做IE的兼容。事件句柄隨著handler插入handles中,為之后的remove做準備。這里的proxy是對于event的擴充,也是添加了當return false時,調(diào)用
preventDefault()和stopPropagation()。

off移除

9

可以看出來,和on是對應的寫法,同樣可以分三部分,只不過這里的功能是移除監(jiān)聽而已。

就直接到了remove,這里主要做的就是根據(jù)傳入的event和selector,用findHandlers進行查找,然后刪除handlers中對應的事件,同時調(diào)用removeEventListener來移除事件處理程序。

$.Event自定義事件

13

這里使用的是createEvent()和initEvent(),這里的事件類型如果不是specialEvents中定義的MouseEvents,就會變成默認Events(DOM3)。
看下自定義的事件對象:
14

可以發(fā)現(xiàn),因為使用compatible()封裝了一下event,所以會有zepto新增的屬性,以及我們傳入的props屬性。

trigger觸發(fā)

我們知道,DOM3級中提供的觸發(fā)事件的api是dispatchEvent()(低版本IE中是fireEvent()),而zepto這里也是一樣:
15

可以看出,對于參數(shù)event為對象時進行了處理,意味著可以直接使用trigger創(chuàng)建+觸發(fā),支持dispatchEvent的情況下,會直接觸發(fā),如果不是DOM節(jié)點,則使用triggerHandler()來觸發(fā)。

triggerHandler觸發(fā)

16

可以看出,如果節(jié)點在之前綁定了其他事件處理程序且使用過stopImmdiatePropagation(),則也不會再觸發(fā)自定義事件。
demo:
17

$.proxy

這其實是個獨立的函數(shù),在add()函數(shù)中使用的proxy,是handler.proxy()函數(shù),與這個無關。這個函數(shù)起到的作用很類似extend,只不過它擴展的是上下文(執(zhí)行環(huán)境)。
我倒是覺得,最關鍵的就是

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

$.proxy.apply(null,args);

自模擬click事件

zepto實際是為了能在手機上輕量級的使用而創(chuàng)造的,為了使用上的不卡頓,手機上不能用click事件,有延時,原因不多說了,寫個小demo,可以看看pc和手機的點擊事件耗時區(qū)別:
touch和click demo
18

如果pc端,最好用chrome,沒大考慮兼容。用開發(fā)者工具也能模擬touch事件。
所以zepto提供了touch模塊,我先自己模擬了下tap事件,連著又自定義了一些事件:
touch和tap demo
19

我的思路是,touch事件觸發(fā)的很快,那么就用touch事件來模擬click,以touchstart和touchend為開始和結束,只考慮了單指的情況,touch事件本身就有:

  • touches:表示當前跟蹤的觸摸操作的touch對象的數(shù)組。
  • targetTouches:特定于事件目標的touch對象的數(shù)組。
  • changeTouches:表示自上次觸摸以來發(fā)生了什么改變的touch對象的數(shù)組。
    這三個數(shù)組里會包含下列屬性:
  • clientX:觸摸目標在視口中的x坐標。
  • clientY:觸摸目標在視口中的y坐標。
  • identifier:標識觸摸的唯一ID。
  • pageX:觸摸目標在頁面中的x坐標。
  • pageY:觸摸目標在頁面中的y坐標。
  • screenX:觸摸目標在屏幕中的x坐標。
  • screenY:觸摸目標在屏幕中的y坐標。
  • target:觸摸的DOM節(jié)點目標。
    在touchstart中可以使用touches,而在touchend里則沒有touches數(shù)組信息了,用changeTouches來獲取觸摸結束時的信息。我主要使用了pageX和pageY,判斷在250ms之下的情況里,如果手指在一個點上(我設了14px大小的范圍,)touchstart加touchend都觸發(fā)了,則為一次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,應該是用來記錄第一個觸發(fā)的點和第二個觸發(fā)的點,果然在監(jiān)聽touchmove事件的回調(diào)里,使用了兩個點來計算偏移,不過這里是將途中所有偏移量都與初始值進行比較,然后匯總。
在touchend中,對于swipe(滑動)和tap(點擊)進行了處理,因為deltaX和deltaY需要在30*30的范圍內(nèi)才會被觸發(fā),但是它的偏移量是move移動的總和,所以在觸發(fā)時容錯率低,也就是不好點出來,相比較其他操作而言。
在touchcancel的處理中,清除所有延遲操作。
同時其整個操作其實是綁定在了document上,所以使用時如果有其他的touch事件也綁定在了doucment上,并且取消了冒泡事件,則之后的所有操作都會失效。

touch的點透

專門寫了下demo來測試點透問題,點透事件的發(fā)生。這里也是之前的click300ms的延時帶來的問題,如果最上層始終存在還好,就怕是點擊消失的情況,那么如果上層用的是touch事件,下層是a標簽,input或者綁定了click事件的節(jié)點,則也會被觸發(fā),只能說zepto的touch事件還需要我們自己來擴充和完善。
在通過對javaScript事件的詳細學習之后,還是有很多途徑去解決這個問題的,比如:

  1. preventDefault(),來取消touchstart和touchend的默認事件。
  2. 給兩層之間加透明的中間層,用于阻止300ms之后的click傳遞到下層中。
  3. pointer-events:none;該css3屬性可以取消節(jié)點上的所有click事件??上g覽器支持不良好23
  4. fastclick,老早就想讀下fastclick如何實現(xiàn)的,正好閱讀一下。

fastclick

使用十分簡單,只要FastClick.attach(document.body);就好,在github上也介紹了另兩種方法,可以不使用fastClick來快速點擊。
24
具體的可以直接去它的github地址看https://github.com/ftlabs/fastclick。
我修改了下之前的例子,新增了一個點擊事件的例子,可以看出,基本比touchend慢1ms。而且使用了click事件,所以也不存在點透事件。

同樣從入口進入,在代碼內(nèi)部,先實例化FastClick(),它有兩個參數(shù):layer和options,layer也就是我們之前傳的document.body,
在FastClick()函數(shù)中,可以先找到
25

相當于document.body上的節(jié)點的click,touch事件的監(jiān)聽都會被FastClick內(nèi)部的事件給處理掉,如果layer上有onclick事件,同樣會被oldOnClick復寫。

在pc上不用多說,綁定操作的節(jié)點肯定先通過對click的監(jiān)聽來處理,
26

這里的trackingClick應該是如果之前頁面中的touchend被UI事件阻塞(可以簡單模擬下,也就是出現(xiàn)字符被選中的情況),重新置空它。
而在手機上,最先觸發(fā)的操作必定是touchstart和touchmove,touchend。
27

這里的targetElement很重要,其實是上面提過的event的target,實際觸摸的節(jié)點目標,這個屬性會在接下來的move,end中用來判斷是否中間換過節(jié)點,并在sendClick中用來真實的觸發(fā)事件
28

返回touchstart中,兼容先不看,這里對于初始節(jié)點也做了存儲,touchStartX,touchStartY。
而接下來的對于double-tap的處理,其實需要看需求,如果是默認值,那么200ms內(nèi)的第二次點擊會禁止觸發(fā)其默認事件event.preventDefault(),在手機上就無法觸發(fā)雙擊事件。

然后就是touchmove,這里倒是很短,只是對于targetElement做了個判斷,是否是同一個觸摸節(jié)點。
29

在touchend中,同樣略過兼容不看,不過我倒是又學到了一個原生api,document.elementFromPoint,傳入x,y來找到節(jié)點,只是認識了一下,實不實用不表。
30

其實一共有三種方式來操作fastclick內(nèi)部自定義的事件,不過當targetTagName=label時,這里focus了一下,把這里的觸發(fā)放到了needsClick中而已,而在這兩塊中,可以知道,如果給className中加入needsFocus和needsClick,則會中斷fastclick,使用原生事件:

  • needsFocus:第一個判斷事件從touchstart到touchend是否超過100ms,則將targetElement置空,其實就是回歸原來的方法,取消fastclick的事件。然后就是focus事件
    31
    這里使用了setSelectionRange(length,length),先選中文本。
    32
    有興趣的可以去MDN看,不過我測試了一下,上面的例子中有個input,其中確實會有http://www.cnblogs.com/vajoy/p/5522114.html#!/follow提到的問題,在快速點擊時光標會直接移到最后面,而上面?zhèn)魅氲膮?shù)都是length,自然就跑到了最后面。
    再通過sendClick來觸發(fā),就會有問題,而且在觸發(fā)完自定義click之后,
    33
    取消了我們點擊時帶的focus操作,這里我先注釋了一下代碼,果然,光標先定到末尾,再定到正確位置,雖然上述博主的方法暫緩此問題,但在有些瀏覽器中仍然存在這一問題,并且實際上仍然是光標定到末尾,再定到正確位置,不如直接不用模擬的focus,作者自己的注釋里說的是ios7中有問題,但focus中用的是deviceISIOS,而不是deviceIsIOSWithBadTarget,改這里也能解決問題。

  • needsClick:這里首先preventDefault,再使用sendClick,上面稍微介紹了下,這里模擬的都是MouseEvents事件,使用targetElement來觸發(fā)。

那么為何fastclick不穿透呢,我自然還是找代碼中哪里用了preventDefaultstopImmediatePropagation,在onMouse中,還是防止快速點擊而加的阻止操作,實際上阻止點透的仍然是needsClick中的event.preventDefault(),去掉之后,頁面點擊仍然會點透。

比較

知道了還是preventDefault()起了作用,我們再回頭看touch模塊,對比一下fastclick,其實兩者的主體層都是差不多的,document和document.body實際在大多數(shù)情況下,可點擊域都是一樣,也就是說兩者都綁定到了最外層上(近似),但fastclick提供了layer和options,意味著可以規(guī)避風險,而zepto的touch模塊則直接綁定到了document之上,至少在使用上,并沒有fastclick方便,不過其定義的各種touch動作,很有意義,在上面說過,在zepto解決點透,可以:

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

在讀源碼時,layer綁定的部分,上面有圖,確實會誤導。因為實際上onClick是可以不走的,之所以在手機上觸發(fā)的仍然是click事件,是因為在sendClick里直接使用dispatchEvent()觸發(fā)了click,所以才會從fastclick定義的this.onClick()中走,其實這一部分可以和zepto一樣放入touchend模塊里面。
這么看,兩個代碼的核心其實大致相同,所不同的是fastclick中還加入的tap事件的focus方法,如果在zepto的touch中直接加入preventDefault(),則input無法獲得焦點,所以可以引入focus事件,從而解決問題
34
35
可能會有兼容性的問題,對于blur()和focus()寫完jQuery之后可以加入兼容性的寫法。
現(xiàn)在就不會點透了。
還可以把touchstart,touchend操作放到具體的節(jié)點上,然后在節(jié)點中進行處理,不過這樣會改動的比較大。zepto中有個方法$.proxy,放在這里有奇效,上面介紹過。

其他

至于說delegate,undelegate,live,die這些代理事件,還有bind,unbind等綁定操作,其實都是on在起作用,不細說。中間去看了fastclick,以及其他事情,其實星期一就寫完了,還是基礎薄弱啊。
之后是看看jQuery的事件操作,壓力山大。

長路漫漫,與君共勉
如果您看完本篇文章感覺不錯,請點擊一下右下角的推薦來支持一下博主,謝謝!