前言
我們今天直接進入事件相關的學習,因為近期可能會改到里面的代碼
就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的核心就看的差不多了,剩下的我們再花一點時間說說就好了