誰說你只是"會用"jQuery?

前言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語句很好的處理了多種使用狀況的參數處理。也許直接看不能知曉究竟是如何作到的,能夠試試每種使用狀況都代入其中,找尋其是如何兼容的。

接下來咱們第三段

這段函數作了很是重要的兩件事

  1. 處理one傳入爲true,事件只觸發一次的場景

  2. 處理傳入了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中的事件處理部分說明白說詳細,歡迎你們提意見。

若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀

若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀

若是對你有一點點幫助,點擊這裏,加一個小星星好很差呀

相關文章
相關標籤/搜索