前言javascript
套用上篇文章向zepto.js學習如何手動觸發DOM事件 的開頭???css
前端在最近幾年實在火爆異常,vue、react、angular各路框架層出不窮,我們要是不知道個雙向數據綁定,不曉得啥是虛擬DOM,也許就被鄙視了。火熱的背後每每也是無盡的浮躁,學習這些先進流行的類庫或者框架可讓咱們走的更快,可是靜下心來回歸基礎,把基石打牢固,卻可讓咱們走的更穩,更遠。html
最近一直在看zepto的源碼,但願經過學習它掌握一些框架設計的技巧,也將好久再也不拾起的js基礎從新溫習鞏固一遍。若是你對這個系列感興趣,歡迎點擊watch
,隨時關注動態。這篇文章主要想說一下zepto中事件模塊(event.js)的添加事件on
以及移除事件off
實現原理,中間會詳細地講解涉及到的細節方面。前端
若是你想看event.js全文翻譯版本,請點擊這裏查看vue
原文地址java
倉庫地址react
在沒有vue和react,甚至angular都沒怎麼接觸的刀耕火種的時代,jQuery或者zepto是咱們手中的利器,是刀刃,他讓咱們遊刃有餘地開發出兼容性好的漂亮的網頁,咱們膜拜並感嘆做者帶來的便利,沉浸其中,沒法自拔。git
可是用了這麼久的zepto你知道這樣寫代碼github
$('.list').on('click', 'li', function (e) { console.log($(this).html()) })
是怎麼實現事件委託的嗎?爲啥此時的this
就是你點中的li
呢?面試
日常咱們可能還會這樣寫。
$('.list li').bind('click', function () {}) $('.list').delegate('li', 'click', function () {}) $('.list li').live('click', function () {}) $('.list li').click(function () {})
寫法有點多,也許你還有其餘的寫法,那麼
on
bind
delegate
live
click()
這些添加事件的形式,有什麼區別,內部之間又有什麼聯繫呢?
相信你在面試過程當中也遇到過相似的問題(看完這邊文章,你能夠知道答案的噢?
)?
接下來咱們從源碼的角度一步步去探究其內部實現的原理。
on
開始爲何選擇從
on
添加事件的方式開始提及,緣由在於其餘寫法幾乎都是on
衍生出來的,明白了on
的實現原理,其餘的也就差很少那麼回事了。
祭出一張畫了很久的圖
上面大概是zepto中on
形式註冊事件的大體流程,好啦開始看源碼啦,首先是on函數,它主要作的事情是註冊事件前的參數處理,真正添加事件是內部函數add。
$.fn.on = function (event, selector, data, callback, one) { // 第一段 var autoRemove, delegator, $this = this if (event && !isString(event)) { $.each(event, function (type, fn) { $this.on(type, selector, data, fn, one) }) return $this } // 第二段 if (!isString(selector) && !isFunction(callback) && callback !== false) callback = data, data = selector, selector = undefined if (callback === undefined || data === false) callback = data, data = undefined if (callback === false) callback = returnFalse // 以上爲針對不一樣的調用形式,作好參數處理 // 第三段 return $this.each(function (_, element) { // 處理事件只有一次生效的狀況 if (one) autoRemove = function (e) { remove(element, e.type, callback) return callback.apply(this, arguments) } // 添加事件委託處理函數 if (selector) delegator = function (e) { var evt, match = $(e.target).closest(selector, element).get(0) if (match && match !== element) { evt = $.extend(createProxy(e), { currentTarget: match, liveFired: element }) return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) } } // 使用add內部函數真正去給選中的元素註冊事件 add(element, event, callback, data, selector, delegator || autoRemove) }) }
直接看到這麼一大坨的代碼不易於理解,咱們分段進行閱讀。
第一段
var autoRemove, delegator, $this = this if (event && !isString(event)) { $.each(event, function (type, fn) { $this.on(type, selector, data, fn, one) }) return $this }
這段代碼主要是爲了處理下面這種調用形式。
$('.list li').on({ click: function () { console.log($(this).html()) }, mouseover: function () { $(this).css('backgroundColor', 'red') }, mouseout: function () { $(this).css('backgroundColor', 'green') } })
這種寫法咱們平時寫的比較少一點,可是確實是支持的。而zepto的處理方式則是循環調用on
方法,以key
爲事件名,val
爲事件處理函數。
在開始第二段代碼閱讀前,咱們先回顧一下,平時常用on
來註冊事件的寫法通常有哪些
// 這種咱們使用的也許最多了 on(type, function(e){ ... }) // 能夠預先添加數據data,而後在回調函數中使用e.data來使用添加的數據 on(type, data, function(e){ ... }) // 事件代理形式 on(type, [selector], function(e){ ... }) // 固然事件代理的形式也能夠預先添加data on(type, [selector], data, function(e){ ... }) // 固然也能夠只讓事件只有一次起效 on(type, [selector], data, function (e) { ... }, true)
還會有其餘的寫法,可是常見的可能就是這些,第二段代碼就是處理這些參數以讓後續的事件正確添加。
第二段
// selector不是字符串形式,callback也不是函數 if (!isString(selector) && !isFunction(callback) && callback !== false) callback = data, data = selector, selector = undefined // 處理data沒有傳或者傳了函數 if (callback === undefined || data === false) callback = data, data = undefined // callback能夠傳false值,將其轉換爲returnFalse函數 if (callback === false) callback = returnFalse
三個if語句很好的處理了多種使用狀況的參數處理。也許直接看不能知曉究竟是如何作到的,能夠試試每種使用狀況都代入其中,找尋其是如何兼容的。
接下來咱們第三段
這段函數作了很是重要的兩件事
處理one傳入爲true,事件只觸發一次的場景
處理傳入了selector,進行事件代理處理函數開發
咱們一件件看它如何實現。
if (one) autoRemove = function (e) { remove(element, e.type, callback) return callback.apply(this, arguments) }
內部用了一個remove
函數,這裏先不作解析,只要知道他就是移除事件的函數就能夠,當移除事件的時候,再執行了傳進來的回調函數。進而實現只調用一次的效果。
那麼事件代理又是怎麼實現咧?
回想一下日常本身是怎麼寫事件代理的,通常是利用事件冒泡(固然也可使用事件捕獲)的性質,將子元素的事件委託到祖先元素身上,不只能夠實現事件的動態性,還能夠減小事件總數,提升性能。
舉個例子
咱們把本來要添加到li上的事件委託到父元素ul上。
<ul class="list"> <li>1</li> <li>2</li> <li>3</li> </ul>
let $list = document.querySelector('.list') $list.addEventListener('click', function (e) { e = e || window.event let target = e.target || e.srcElement if (target.tagName.toLowerCase() === 'li') { target.style.background = 'red' } }, false)
回到第三段
if (selector) delegator = function (e) { // 這裏用了closest函數,查找到最早符合selector條件的元素 var evt, match = $(e.target).closest(selector, element).get(0) // 查找到的最近的符合selector條件的節點不能是element元素 if (match && match !== element) { // 而後將match節點和element節點,擴展到事件對象上去 evt = $.extend(createProxy(e), { currentTarget: match, liveFired: element }) // 最後即是執行回調函數 return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1))) } }
zepto中實現事件代理的基本原理是:以當前目標元素e.target
爲起點向上查找到最早符合selector
選擇器規則的元素,而後擴展了事件對象,添加了一些屬性,最後以找到的match元素做爲回調函數的內部this
做用域,並將擴展的事件對象做爲回調函數的第一個參數傳進去執行。
這裏須要知道.closest(...)
api的具體使用,若是你不太熟悉,請點擊這裏查看
說道這裏,事件尚未添加啊!到底在哪裏添加的呢,on函數的最後一句,即是要進入事件添加了。
add(element, event, callback, data, selector, delegator || autoRemove)
zepto的內部真正給元素添加事件的地方在add函數。
function add(element, events, fn, data, selector, delegator, capture) { var id = zid(element), set = (handlers[id] || (handlers[id] = [])) events.split(/\s/).forEach(function (event) { if (event == 'ready') return $(document).ready(fn) var handler = parse(event) handler.fn = fn handler.sel = selector // emulate mouseenter, mouseleave if (handler.e in hover) fn = function (e) { var related = e.relatedTarget if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments) } handler.del = delegator var callback = delegator || fn handler.proxy = function (e) { e = compatible(e) if (e.isImmediatePropagationStopped()) return e.data = data var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args)) if (result === false) e.preventDefault(), e.stopPropagation() return result } handler.i = set.length set.push(handler) if ('addEventListener' in element) element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) }) }
個人神,又是這麼長長長長的一大坨,人艱不拆,看着心累啊啊啊啊!!!
不過不用急,只要一步步去看,最終確定能夠看懂的。
開頭有一句話
var id = zid(element)
function zid(element) { return element._zid || (element._zid = _zid++) }
zepto中會給添加事件的元素身上加一個惟一的標誌,_zid從1開始不斷往上遞增。後面的事件移除函數都是基於這個id來和元素創建關聯的。
// 代碼初始地方定義 var handlers = {}, set = (handlers[id] || (handlers[id] = []))
handlers
即是事件緩衝池,以數字0, 1, 2, 3...保存着一個個元素的事件處理程序。來看看handlers長啥樣。
html
<ul class="list"> <li>1</li> <li>2</li> <li>3</li> </ul>
javascript
$('.list').on('click', 'li', '', function (e) { console.log(e) }, true)
以上截圖即是這段代碼執行後獲得的handlers,其自己是個對象,每一個key(1, 2, 3 ...)(這個key也是和元素身上的_zid屬性一一對應的)都保存着一個數組,而數組中的每一項目都保存着一個與事件類型相關的對象。咱們來看看,每一個key的數組都長啥樣
[ { e: 'click', // 事件名稱 fn: function () {}, // 用戶傳入的回調函數 i: 0, // 該對象在該數組中的索引 ns: 'qianlongo', // 命名空間 proxy: function () {}, // 真正給dom綁定事件時執行的事件處理程序, 爲del或者fn sel: '.qianlongo', // 進行事件代理時傳入的選擇器 del: function () {} // 事件代理函數 }, { e: 'mouseover', // 事件名稱 fn: function () {}, // 用戶傳入的回調函數 i: 1, // 該對象在該數組中的索引 ns: 'qianlongo', // 命名空間 proxy: function () {}, // 真正給dom綁定事件時執行的事件處理程序, 爲del或者fn sel: '.qianlongo', // 進行事件代理時傳入的選擇器 del: function () {} // 事件代理函數 } ]
這樣的設置給後面事件的移除帶了很大的便利。畫個簡單的圖,看看元素添加的事件和handlers中的映射關係。
明白了他們之間的映射關係,咱們再回到源碼處,繼續看。
events.split(/\s/).forEach(function (event) { // xxx })
暫時去除了一些內部代碼邏輯,咱們看到其對event
作了切分,並循環添加事件,這也是咱們像下面這樣添加事件的緣由
$('li').on('click mouseover mouseout', function () {})
那麼接下來咱們要關注的就是循環的內部細節了。添加了部分註釋
// 若是是ready事件,就直接調用ready方法(這裏的return貌似沒法結束forEach循環吧) if (event == 'ready') return $(document).ready(fn) // 獲得事件和命名空間分離的對象 'click.qianlongo' => {e: 'click', ns: 'qianlongo'} var handler = parse(event) // 將用戶輸入的回調函數掛載到handler上 handler.fn = fn // 將用戶傳入的選擇器掛載到handler上(事件代理有用) handler.sel = selector // 用mouseover和mouseout分別模擬mouseenter和mouseleave事件 // https://qianlongo.github.io/zepto-analysis/example/event/mouseEnter-mouseOver.html(mouseenter與mouseover爲什麼這般糾纏不清?) // emulate mouseenter, mouseleave if (handler.e in hover) fn = function (e) { var related = e.relatedTarget if (!related || (related !== this && !$.contains(this, related))) return handler.fn.apply(this, arguments) } handler.del = delegator // 注意須要事件代理函數(通過一層處理事後的)和用戶輸入的回調函數優先使用事件代理函數 var callback = delegator || fn // proxy是真正綁定的事件處理程序 // 而且改寫了事件對象event // 添加了一些方法和屬性,最後調用用戶傳入的回調函數,若是該函數返回false,則認爲須要阻止默認行爲和阻止冒泡 handler.proxy = function (e) { e = compatible(e) if (e.isImmediatePropagationStopped()) return e.data = data var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args)) // 若是回調函數返回false,那麼將阻止冒泡和阻止瀏覽器默認行爲 if (result === false) e.preventDefault(), e.stopPropagation() return result } // 將該次添加的handler在set中的索引賦值給i handler.i = set.length // 把handler保存起來,注意由於一個元素的同一個事件是能夠添加多個事件處理程序的 set.push(handler) // 最後固然是綁定事件 if ('addEventListener' in element) element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
至此,添加事件到這裏告一段落了。讓咱們再回到文章初始的問題,
on
bind
delegate
live
click()
這些添加事件的形式,有什麼區別,內部之間又有什麼聯繫呢?其實看他們的源碼大概就知道區別
// 綁定事件 $.fn.bind = function (event, data, callback) { return this.on(event, data, callback) } // 小範圍冒泡綁定事件 $.fn.delegate = function (selector, event, callback) { return this.on(event, selector, callback) } // 將事件冒泡代理到body上 $.fn.live = function (event, callback) { $(document.body).delegate(this.selector, event, callback) return this } // 綁定以及觸發事件的快件方式 // 好比 $('li').click(() => {}) ; ('focusin focusout focus blur load resize scroll unload click dblclick ' + 'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' + 'change select keydown keypress keyup error').split(' ').forEach(function (event) { $.fn[event] = function (callback) { return (0 in arguments) ? // click() 形式的調用內部仍是用了bind this.bind(event, callback) : this.trigger(event) } })
bind和click()函數都是直接將事件綁定到元素身上,live則代理到body元素身上,delegate是小範圍是事件代理,性能在因爲live,on就最厲害了,以上函數均可以用on實現調用。
事件移除的實現有賴於事件綁定的實現,綁定的時候,把真正註冊的事件信息都和dom關聯起來放在了handlers中,那麼移除具體是如何實現的呢?咱們一步步來看。
一樣先放一張事件移除的大體流程圖
off函數
$.fn.off = function (event, selector, callback) { var $this = this // {click: clickFn, mouseover: mouseoverFn} // 傳入的是對象,循環遍歷調用自己解除事件 if (event && !isString(event)) { $.each(event, function (type, fn) { $this.off(type, selector, fn) }) return $this } // ('click', fn) if (!isString(selector) && !isFunction(callback) && callback !== false) callback = selector, selector = undefined if (callback === false) callback = returnFalse // 循環遍歷刪除綁定在元素身上的事件,如何解除,能夠看remove return $this.each(function () { remove(this, event, callback, selector) }) }
off函數基本上和on函數是一個套路,先作一些基本的參數解析,而後把移除事件的具體工做交給remove函數實現,因此咱們主要看remove函數。
remove函數
// 刪除事件,off等方法底層用的該方法 function remove(element, events, fn, selector, capture) { // 獲得添加事件的時候給元素添加的標誌id var id = zid(element) // 循環遍歷要移除的事件(因此咱們用的時候,能夠一次性移除多個事件) ; (events || '').split(/\s/).forEach(function (event) { // findHandlers返回的是符合條件的事件響應集合 findHandlers(element, event, fn, selector).forEach(function (handler) { // [{}, {}, {}]每一個元素添加的事件形如該結構 // 刪除存在handlers上的響應函數 delete handlers[id][handler.i] // 真正刪除綁定在element上的事件及其事件處理函數 if ('removeEventListener' in element) element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture)) }) }) }
繼續往下走,一個重要的函數findHandlers
// 根據給定的element、event等參數從handlers中查找handler, // 主要用於事件移除(remove)和主動觸發事件(triggerHandler) function findHandlers(element, event, fn, selector) { // 解析event,從而獲得事件名稱和命名空間 event = parse(event) if (event.ns) var matcher = matcherFor(event.ns) // 讀取添加在element身上的handler(數組),並根據event等參數帥選 return (handlers[zid(element)] || []).filter(function (handler) { return handler && (!event.e || handler.e == event.e) // 事件名須要相同 && (!event.ns || matcher.test(handler.ns)) // 命名空間須要相同 && (!fn || zid(handler.fn) === zid(fn)) // 回調函數須要相同(話說爲何經過zid()這個函數來判斷呢?) && (!selector || handler.sel == selector) // 事件代理時選擇器須要相同 }) }
由於註冊事件的時候回調函數不是用戶傳入的fn,而是自定義以後的proxy函數,因此須要將用戶此時傳入的fn和handler中保存的fn相比較是否相等。
羅裏吧嗦說了好多,不知道有沒有把zepto中的事件處理部分說明白說詳細,歡迎你們提意見。
若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀
若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀
若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀