【zepto學習筆記03】事件機制


前言

我們今天直接進入事件相關的學習,因為近期可能會改到里面的代碼
就zepto來說,我認為最重要的就是選擇器與事件相關了,隨着瀏覽器升級,選擇器簡單了,而事件相關仍然是核心,今天我們就來學習學習

zepto事件處理部分篇幅不大,不到400行,前面篇幅也很小,所以真的很適合移動開發

變量定義

1 var $$ = $.zepto.qsa,
2 handlers = {}, _zid = 1,
3 specialEvents = {},
4 hover = {
5     mouseenter: 'mouseover',
6     mouseleave: 'mouseout'
7 }

事件部分首先定義了幾個變量,$$為zepto選擇器的方法,暫時不管他(據觀察,好像也沒有地方用到了,所以無意義)
handlers為一個對象,與_zid息息相關,暫時不知道干什么的(據猜測兩個應該是保存函數句柄,為removeEvent做准備)

hover應該會同時觸發兩個事件才會觸發,我們這里先不管,繼續往下看

這里提供一個zid飛方法,該方法用於保證-zid的唯一性

1 function zid(element) {
2     return element._zid || (element._zid = _zid++)
3 }

算了,我們這里還是用一個實際點的方法跟進來吧,首先說一個關鍵的

$.Event(type, [properties])

這個方法比較關鍵,他可以創建一個dom事件,我們可以使用他來擴展新的事件對象,默認情況事件是可以冒泡的,可以設置
這個事件可以通過trigger觸發
PS:trigger與Event是關鍵,各位一定要搞懂

 1 specialEvents = {}
 2 specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'
 3 
 4 //根據參數創建一個event對象
 5 $.Event = function (type, props) {
 6     //當type是個對象時
 7     if (typeof type != 'string') props = type, type = props.type
 8     //創建一個event對象,如果是click,mouseover,mouseout時,創建的是MouseEvent,bubbles為是否冒泡
 9     var event = document.createEvent(specialEvents[type] || 'Events'),
10     bubbles = true
11     //確保bubbles的值為true或false,並將props參數的屬性擴展到新創建的event對象上
12     if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
13     //初始化event對象,type為事件類型,如click,bubbles為是否冒泡,第三個參數表示是否可以用preventDefault方法來取消默認操作
14     event.initEvent(type, bubbles, true, null, null, null, null, null, null, null, null, null, null, null, null)
15     //添加isDefaultPrevented方法,event.defaultPrevented返回一個布爾值,表明當前事件的默認動作是否被取消,也就是是否執行了 event.preventDefault()方法.
16     event.isDefaultPrevented = function () {
17         return this.defaultPrevented
18     }
19     return event
20 }

這個方法的第二個參數我一般沒看到出現,有時候我們可能會直接傳入一個事件對象作為參數,所以我們這個時候第二個參數要保存第一個對象參數
並將type獲得,type還是必須是一個字符串

然后此處創建了一個事件對象:

var event = document.createEvent(specialEvents[type] || 'Events'),

createEvent

Event對象的屬性提供了有關事件的細節(比如事件發生到什么元素上),並且Event對象可以控制事件傳播
PS:IE這個家伙我已經不願意去關注他了,標准事件模型中,Event對象會傳遞給句柄函數,但是IE會保存到window.event 中

Event具有以下屬性:

event.bubbles

事件冒泡類型,是冒泡則為true,非冒泡就是false

事件傳播分為三個階段:
① 捕獲階段,事件由dom對象沿着文檔樹向下傳播給目標節點,如果目標元素(或者parent)注冊了事件,那么在事件傳播過程中會執行
② 此階段發生在目標節點自身,直接注冊到目標上的適合事件會運行(比如本來要觸發click,如果注冊了mousedown也會運行)
③ 冒泡階段,此階段事件從目標元素向上傳播到冒泡元素或者document

所以我們在我們的回調函數中打印event.bubbles,如果是true的話,就說明是冒泡執行的事件

cancelable

該屬性與我們的preventDefault有很大關聯,事實上,preventDefault就是將該屬性設置為false(也許吧......)

event.currentTarget

該屬性返回監聽事件的節點,即當前處理該事件的元素,在冒泡或捕獲階段,該元素比較有用
比如我們將事件綁定到了div上,但是我們是點擊里面的span而觸發了div上面的事件,這個div就是currentTarget了

eventPhase

eventPhase 屬性返回事件傳播的當前階段。它的值是下面的三個常量之一,它們分別表示捕獲階段、正常事件派發和起泡階段

event.targettarget

事件屬性可返回事件的目標節點(觸發該事件的節點),如生成事件的元素、文檔或窗口。

以上面的例子為例,此時的target就是我們的span了

event.timeStamp

該屬性,返回事件生成的日期

event.type

type 事件屬性返回發生的事件的類型,即當前 Event 對象表示的事件的名稱。
它與注冊的事件句柄同名,或者是事件句柄屬性刪除前綴 "on" 比如 "submit"、"load" 或 "click"。

這里比較關鍵了,如果我們想注冊自己的事件,比如tap,就可以這么干了,好了繼續我們的話題

event.stopPropagation()

該方法將停止事件的傳播,阻止它被分派到其他 Document 節點。在事件傳播的任何階段都可以調用它。注意,雖然該方法不能阻止同一個 Document 節點上的其他事件句柄被調用,但是它可以阻止把事件分派到其他節點。

event.preventDefault()

該方法將通知 Web 瀏覽器不要執行與事件關聯的默認動作(如果存在這樣的動作)。例如,如果 type 屬性是 "submit",在事件傳播的任意階段可以調用任意的事件句柄,通過調用該方法,可以阻止提交表單。注意,如果 Event 對象的 cancelable 屬性是 fasle,那么就沒有默認動作,或者不能阻止默認動作。無論哪種情況,調用該方法都沒有作用。
比如,我們點擊一個input時候在手機上會彈出鍵盤,我們如果使用e.preventDefault();就不會彈出

event.initEvent(eventType,canBubble,cancelable)
initEvent() 方法初始化新事件對象的屬性。該方法必須在dispatchEvent調用前執行才有效

PS:其實不止initEvent方法,還有initMouseEvent方法,這個方法還會提供鼠標坐標

/*
typeArg - 指定事件類型。
canBubbleArg - 指定該事件是否可以 bubble。
cancelableArg - 指定是否可以阻止事件的默認操作。
viewArg - 指定 Event 的 AbstractView。
detailArg - 指定 Event 的鼠標單擊量。
screenXArg - 指定 Event 的屏幕 x 坐標
screenYArg - 指定 Event 的屏幕 y 坐標
clientXArg - 指定 Event 的客戶機 x 坐標
clientYArg - 指定 Event 的客戶機 y 坐標
ctrlKeyArg - 指定是否在 Event 期間按下 control 鍵。
altKeyArg - 指定是否在 Event 期間按下 alt 鍵。
shiftKeyArg - 指定是否在 Event 期間按下 shift 鍵。
metaKeyArg - 指定是否在 Event 期間按下 meta 鍵。
buttonArg - 指定 Event 的鼠標按鍵。
relatedTargetArg - 指定 Event 的相關 EventTarget。
*/
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);

這個方法的應用我們后面點會涉及,這里暫時就不管他了,這里回到我們的Event方法

他這里根據傳入參數(type),創建了一個Event對象,此處的props如果是對象,那么就將賦給新建的Event對象
然后使用initEvent初始化了我們Event參數的屬性,並且添加了isDefaultPrevented方法,該方法可以知道當前事件的默認動作是否取消
然后將這個對象返回
既然都到了這里,我們來自定義一個鼠標事件吧

自定義鼠標事件

我們知道在移動端的click事件響應很慢,所以我們這里自己來實現一個fastclick的事件來替換本身的click

http://sandbox.runjs.cn/show/scbpb0xg

請使用手機測試點擊相應速度

 1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head>
 3     <title></title>
 4      <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
 5     <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/js/sandbox/other/zepto.min.js"></script>
 6     <script src="../../zepto.js" type="text/javascript"></script>
 7 </head>
 8 <body>
 9     <input type="button" value="我是普通點擊事件" id="click" />
10     <input type="button" value="我是快速點擊事件" id="fastclick" />
11 </body>
12 <script type="text/javascript">
13     var c = $('#click');
14     var fc = $('#fastclick');
15     var t = new Date().getTime()
16     $(document).bind('touchstart', function (e) {
17         t = e.timeStamp;
18     });
19     $(document).bind('touchend', function (e) {
20         var event = $.Event('fastclick');
21         //這里為了方便而已,其實該e.target
22         fc[0].dispatchEvent(event);
23     });
24     c.bind('click', function (e) {
25         $(this).val('我是普通點擊事件' + '(' + (e.timeStamp - t) + ')')
26     });
27     fc.bind('fastclick', function (e) {
28         $(this).val('我是快速點擊事件' + '(' + (e.timeStamp - t) + ')')
29     })
30 </script>
31 </html>

我們這里為第二個按鈕定義了一個fastclick事件,然后在touchend時候觸發了該dom的事件
大家點擊時候自己就可以看到響應的速度(請使用手機測試)
PS:這里各位一定要對dispatchEvent了解哦

好了,我們繼續回到我們的代碼

trigger/triggerHandler(event, [data])  

這個trigger和dispatchEvent有莫大的關系,通過他我們可以觸發一個事件,比如上面自定義事件可以通過他觸發,我們看看他的源碼

 1 $.fn.trigger = function (event, data) {
 2     if (typeof event == 'string' || $.isPlainObject(event)) event = $.Event(event)
 3     fix(event)
 4     event.data = data
 5     return this.each(function () {
 6         // items in the collection might not be DOM elements
 7         // (todo: possibly support events on plain old objects)
 8         if ('dispatchEvent' in this) this.dispatchEvent(event)
 9     })
10 }

他這里有個fix方法用於修復event對象,

 1 function fix(event) {
 2     if (!('defaultPrevented' in event)) {
 3         event.defaultPrevented = false //初始值false
 4         var prevent = event.preventDefault // 引用默認preventDefault
 5         event.preventDefault = function () { //重寫preventDefault
 6             this.defaultPrevented = true
 7             prevent.call(this)
 8         }
 9     }
10 }

修復后,他就遍歷我們包裝的dom集合,然后依次觸發該事件

triggerHandler

下面有一個triggerHandler事件與他相似,但是不會冒泡

 1 //觸發元素上綁定的指定類型的事件,但是不冒泡
 2 $.fn.triggerHandler = function (event, data) {
 3     var e, result
 4     this.each(function (i, element) {
 5         e = createProxy(typeof event == 'string' ? $.Event(event) : event)
 6         e.data = data
 7         e.target = element
 8         //遍歷元素上綁定的指定類型的事件處理函數集,按順序執行,如果執行過stopImmediatePropagation,
 9         //那么e.isImmediatePropagationStopped()就會返回true,再外層函數返回false
10         //注意each里的回調函數指定返回false時,會跳出循環,這樣就達到的停止執行回面函數的目的
11         $.each(findHandlers(element, event.type || event), function (i, handler) {
12             result = handler.proxy(e)
13             if (e.isImmediatePropagationStopped()) return false
14         })
15     })
16     return result
17 }

這里有個findHandlers方法,我們來看看

 1 //查找綁定在元素上的指定類型的事件處理函數集合
 2 function findHandlers(element, event, fn, selector) {
 3     event = parse(event)
 4     if (event.ns) var matcher = matcherFor(event.ns)
 5     return (handlers[zid(element)] || []).filter(function (handler) {
 6         return handler && (!event.e || handler.e == event.e) //判斷事件類型是否相同
 7     &&
 8     (!event.ns || matcher.test(handler.ns)) //判斷事件命名空間是否相同
 9         //注意函數是引用類型的數據zid(handler.fn)的作用是返回handler.fn的標示符,如果沒有,則給它添加一個,
10         //這樣如果fn和handler.fn引用的是同一個函數,那么fn上應該也可相同的標示符,
11         //這里就是通過這一點來判斷兩個變量是否引用的同一個函數
12     &&
13     (!fn || zid(handler.fn) === zid(fn)) && (!selector || handler.sel == selector)
14     })
15 }

我們前面說了,handlers應該保存的是我們的事件句柄集合,而我們可以東莞zid獲取dom的唯一標識_zid
PS:zepto將該屬性保存至dom上,是因為dom屬性不會變化可以幫助remove時候找到事件句柄

其中使用了filter方法過來已有的集合,只取出與傳入的事件(event)對象相同的事件(命名空間那些高級東西我們暫時不管吧)

然后下面使用了proxy方法,我們來看看

 1 //設置代理
 2 $.proxy = function (fn, context) {
 3     if ($.isFunction(fn)) {
 4         //如果fn是函數,則申明一個新的函數並用context作為上下文調用fn
 5         var proxyFn = function () {
 6             return fn.apply(context, arguments)
 7         }
 8         //引用fn標示符
 9         proxyFn._zid = zid(fn)
10         return proxyFn
11     } else if (typeof context == 'string') {
12         return $.proxy(fn[context], fn)
13     } else {
14         throw new TypeError("expected function")
15     }
16 }

PS:我這里差點被坑了,我差點看成$.proxy方法了,其實不是的,這個handler.proxy是注冊事件時候搞的一個東西,我們后面點說注冊

1 handler.proxy = function (e) {
2     var result = callback.apply(element, [e].concat(e.data))
3     //當事件處理函數返回false時,阻止默認操作和冒泡
4     if (result === false) e.preventDefault(), e.stopPropagation()
5     return result
6 }

嗯,由於這兩的觸發與注冊事件相關,我們放到后面點再說,而且我發現triggerHandler沒地方用的感覺......

$.proxy(fn, context) 

接受一個函數,然后返回一個新函數,並且這個新函數始終保持了特定的上下文語境,新函數中this指向context參數。另外一種形式,原始的function是context對像的方法。

 1 //設置代理
 2 $.proxy = function (fn, context) {
 3     if ($.isFunction(fn)) {
 4         //如果fn是函數,則申明一個新的函數並用context作為上下文調用fn
 5         var proxyFn = function () {
 6             return fn.apply(context, arguments)
 7         }
 8         //引用fn標示符
 9         proxyFn._zid = zid(fn)
10         return proxyFn
11     } else if (typeof context == 'string') {
12         return $.proxy(fn[context], fn)
13     } else {
14         throw new TypeError("expected function")
15     }
16 }

代理其實就是通過apply或者call改變javascript上下文作用域而已,我們不關注了,接下來就來到我們關鍵的地方了

事件注冊

我們有很多方法可以為dom注冊事件,而zepto為我們准備了統一的出口

bind/one/delegate/live/on 都可以綁定事件,但是他們最終都是調用了add方法

其中live私下是調用delegate,而且我們推薦使用on所以我們這里主要關注on吧,反正統一接口是add

1 $.fn.on = function (event, selector, callback) {
2     return !selector || $.isFunction(selector) ? this.bind(event, selector || callback) : this.delegate(selector, event, callback)
3 }
4 
5 $.fn.bind = function (event, callback) {
6     return this.each(function () {
7         add(this, event, callback)
8     })
9 }
 1 //給元素綁定監聽事件,可同時綁定多個事件類型,如['click','mouseover','mouseout'],也可以是'click mouseover mouseout'
 2 function add(element, events, fn, selector, getDelegate, capture) {
 3     var id = zid(element),
 4       set = (handlers[id] || (handlers[id] = [])) //元素上已經綁定的所有事件處理函數
 5     eachEvent(events, fn, function (event, fn) {
 6         var handler = parse(event)
 7         //保存fn,下面為了處理mouseenter, mouseleave時,對fn進行了修改
 8         handler.fn = fn
 9         handler.sel = selector
10         // 模仿 mouseenter, mouseleave
11         if (handler.e in hover) fn = function (e) {
12             /* 
13             relatedTarget為事件相關對象,只有在mouseover和mouseout事件時才有值
14             mouseover時表示的是鼠標移出的那個對象,mouseout時表示的是鼠標移入的那個對象
15             當related不存在,表示事件不是mouseover或者mouseout,mouseover時!$.contains(this, related)當相關對象不在事件對象內
16             且related !== this相關對象不是事件對象時,表示鼠標已經從事件對象外部移入到了對象本身,這個時間是要執行處理函數的
17             當鼠標從事件對象上移入到子節點的時候related就等於this了,且!$.contains(this, related)也不成立,這個時間是不需要執行處理函數的
18             */
19             var related = e.relatedTarget
20             if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments)
21         }
22         //事件委托
23         handler.del = getDelegate && getDelegate(fn, event)
24         var callback = handler.del || fn
25         handler.proxy = function (e) {
26             var result = callback.apply(element, [e].concat(e.data))
27             //當事件處理函數返回false時,阻止默認操作和冒泡
28             if (result === false) e.preventDefault(), e.stopPropagation()
29             return result
30         }
31         //設置處理函數的在函數集中的位置
32         handler.i = set.length
33         //將函數存入函數集中
34         set.push(handler)
35         element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
36     })
37 }

我們這里來詳細看看我們的add方法

① 首先根據活動唯一的id(根據zid與dom而來)
這里有個地方要注意,因為我們對統一dom可以注冊多個事件,而他們的zid是相同的哦
② 取出元素上已經綁定的事件,第一次肯定是空數組
③ 第一個參數為dom,第二個為事件名稱,第三個為回調函數,后面的我們暫時不關注
④ 使用eachEvent方法,該方法會遍歷事件

1 function eachEvent(events, fn, iterator) {
2     if ($.type(events) != "string") $.each(events, iterator)
3     else events.split(/\s/).forEach(function (type) {
4         iterator(type, fn)
5     })
6 }

該方法具有三個參數,第一個參數為事件名(可以傳入多個事件比如:"click mousemove")
第二個參數是本身的回調函數,會在第三個參數(回調函數中作為參數傳入)
PS:總之他就是個遍歷處理函數,你的typeName與回調函數會傳進去
⑤ 使用parse解析事件類型

1 //解析事件類型,返回一個包含事件名稱和事件命名空間的對象
2 function parse(event) {
3     var parts = ('' + event).split('.')
4     return {
5         e: parts[0],
6         ns: parts.slice(1).sort().join(' ')
7     }
8 }

PS:為什么會有這個方法呢,因為我們使用backbone綁定事件時候是這個樣子的click.delegateEvents......

完了將回調函數與選擇器(如果有的話)賦予handler,這里多的我們暫時不管
⑥ i表示當前函數在dom函數集中的位置,然后將他壓入set,因為set為引用,所以原來handlers[id]的值也變了
⑦ 調用addEventListener綁定事件
element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))

至此事件綁定就結束了,我們順便看看remove,因為所有的注銷事件都是通過remove

注銷事件

 1 $.fn.off = function (event, selector, callback) {
 2     return !selector || $.isFunction(selector) ? this.unbind(event, selector || callback) : this.undelegate(selector, event, callback)
 3 }
 4 
 5 $.fn.unbind = function (event, callback) {
 6     return this.each(function () {
 7         remove(this, event, callback)
 8     })
 9 }
10 
11 function remove(element, events, fn, selector, capture) {
12     var id = zid(element)
13     eachEvent(events || '', fn, function (event, fn) {
14         findHandlers(element, event, fn, selector).forEach(function (handler) {
15             delete handlers[id][handler.i]
16             element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
17         })
18     })
19 }

我們直接對准remove開炮
① 首先根據dom獲取唯一id
② 調用eachEvent方法依次處理
③ 根據findHandler找到當前type的事件類型集合
④ 刪除數據句柄,然后移除dom事件

這個比較簡單,我們就不詳說了

簡單寫法

我們經常這樣綁定事件:
el.click(function () {})
那么這樣是如何綁定事件的呢?答案在此

 1 ('focusin focusout load resize scroll unload click dblclick ' +
 2     'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' +
 3     'change select keydown keypress keyup error').split(' ').forEach(function (event) {
 4         $.fn[event] = function (callback) {
 5             return callback ?
 6             //如果有callback回調,則認為它是綁定
 7       this.bind(event, callback) :
 8             //如果沒有callback回調,則讓它主動觸發
 9       this.trigger(event)
10         }
11     })

以click來說相當於

1 $.fn.click = function (fn) {
2 this.bind('click', callback)
3 }

所以才綁定事件的啦......

結語

好了,今天暫時到這里,至此我們將zepto的核心就看的差不多了,剩下的我們再花一點時間說說就好了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM