前言
前端在最近幾年實在火爆異常,vue、react、angular各路框架層出不窮,我們們要是不知道個雙向資料繫結,不曉得啥是虛擬DOM,也許就被鄙視了。火熱的背後往往也是無盡的浮躁,學習這些先進流行的類庫或者框架可以讓我們走的更快,但是靜下心來回歸基礎,把基石打牢固,卻可以讓我們走的更穩,更遠。
最近一直在看zepto的原始碼,希望通過學習它掌握一些框架設計的技巧,也將很久不再拾起的js基礎重新溫習鞏固一遍。如果你對這個系列感興趣,歡迎點選下方地址watch
,隨時關注動態。這篇文章主要想說一下zepto中事件模組(event.js)的trigger
實現原理。
event.js模組
zepto中由許多小的模組組合合成,基礎的
zepto.js
模組,event.js
事件處理模組,ajax.js
請求處理模組等等。其中event.js
事件處理模組的核心完成了zepto中事件繫結on
,移除off
還有手動觸發trigger
等功能。我們簡單回顧下如何使用zepto的這三大功能。
<ul class="list">
<li>1</li>
<li>2</li>
</ul>複製程式碼
let $list = $('.list')
let cb1 = function (e, name) {
console.log(1, name)
}
let cb2 = function (e, name) {
console.log(2, name)
}
$list.on('click', cb1)
$list.on('click', cb2)
// 移除事件
// 我們可以指定移除click事件的某個事件處理程式
$list.off('click', cb1)
// 也可以直接移除click事件
$list.off('click')
// 手動觸發事件
$list.trigger('click', 'qianlongo')複製程式碼
哥們你逗我呢,jQuery,zepto多熟了,誰不會用這個啊!客觀別急,我們今天主要是慢慢來看看它原始碼怎麼實現的。
一步步看trigger怎麼實現
直接上程式碼
$.fn.trigger = function (event, args) {
// 對傳入的event進行處理,如果是字串或者純物件,得到一個自己建立的事件物件
// 如果傳入的已經是個經過$.Event處理的物件,則放入compatible再次改造(其實就是新增了幾個方法,和重寫了幾個方法)
event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)
// args傳遞給事件處理程式的引數
event._args = args
return this.each(function () {
// handle focus(), blur() by calling them directly
if (event.type in focus && typeof this[event.type] == "function") this[event.type]()
// items in the collection might not be DOM elements
// 觸發dom事件
else if ('dispatchEvent' in this) this.dispatchEvent(event)
// 因為zepto物件內部的元素不一定是dom元素,此時直接觸發回撥函式
else $(this).triggerHandler(event, args)
})
}複製程式碼
直接貼出trigger函式的程式碼可能我們是懵逼的
$
是啥啊!!!
$.fn
是啥啊!!!
$.isPlainObject
又是啥啊!!!
$.Event
又是什麼鬼?
彷彿有一連串的問題等待著我們解決。
為了直接切入不易理解,我們先來看看zepto中是如何給基礎的zepto.js
模組新增功能的
首先看看zepto.js模組
var Zepto = (function () {
// xxxx
var $ = function (selector, context) {
return zepto.init(selector, context)
}
return $
zepto.Z.prototype = Z.prototype = $.fn
// xxxx
})()
window.Zepto = Zepto
window.$ === undefined && (window.$ = Zepto)複製程式碼
儘量刪除了一些不必要的程式碼,可以看到我們平時使用的Zepto
其實就是其匿名函式自執行內部匯出的一個函式。而$.fn
就是其原型
如何給zepto.js模組增添功能
zepto.js
模組只有一些基礎的功能,比如給dom新增事件的功能就沒有,怎麼新增呢?
(function ($) {
// xxx
$.fn.on = function () {//xxxx}
$.fn.off = function () {//xxxx}
$.fn.trigger = function () {//xxxx}
$.Event = function () {//xxx}
// xxx
})(Zepto)複製程式碼
最後縮減掉其他的干擾程式碼,可以看到所謂的給zepto.js
模組增添功能,基本上就是在其原型上新增新的方法或者直接在$函式上定一些靜態方法。
好啦我們已經解決了$
,$.fn
是啥的疑問了,現在回去開始一步步解讀如何實現手動觸發事件。
重新看trigger函式原始碼
$.fn.trigger = function (event, args) {
// 對傳入的event進行處理,如果是字串或者純物件,得到一個自己建立的事件物件
// 如果傳入的已經是個經過$.Event處理的物件,則放入compatible再次改造(其實就是新增了幾個方法,和重寫了幾個方法)
event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)
// args傳遞給事件處理程式的引數
event._args = args
// xxx
}複製程式碼
先把後面的一些程式碼給刪除了,我們先理解這幾句程式碼。其中非常重要的一個函式就是$.Event
,至於
isString
=> 判斷是不是字串
isPlainObject
=> 判斷是不是存粹的物件(必須是物件,window物件除外,該物件的原型必須和Object的原型一致)
compatible
=> 其實就是對事件物件event
做一些擴充套件,比如新增一些方法,重寫一些方法之類的。
這幾個方法暫時可以不需要太多關心.
我們主要看看$.Event
,這裡面幾乎含有如何手動觸發一個dom事件的大部分步驟和內容。
我們主要看看$.Event
,這裡面幾乎含有如何手動觸發一個dom事件的大部分步驟和內容。
我們主要看看$.Event
,這裡面幾乎含有如何手動觸發一個dom事件的大部分步驟和內容。
$.Event是個啥
建立並初始化一個指定的dom事件物件, 如果給定了props,則將其擴充套件到事件物件上
$.Event = function (type, props) {
// 當type是個物件時,比如{type: 'click', data: 'qianlongo'}
if (!isString(type)) props = type, type = props.type
// click,mousedown,mouseup mousemove對應MouseEvent
// 其他事件對應為Events
// 並把bubbles設定為true,表示事件冒泡,為false則不冒泡
var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true
// 當props存在的時候,對props進行迴圈處理,將其屬性擴充套件到event物件上
if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
// 初始化事件物件,第一個為事件型別,第二個為冒泡與否,第三個為是否可以通過preventDefault來阻止瀏覽器預設行為
event.initEvent(type, bubbles, true)
// 再對創造出來的時間物件處理一番並返回
return compatible(event)
}複製程式碼
註釋已經寫的很清楚了,這個函式就是返回一個經過初始化了的事件物件
到這裡我們直接歸納一下要手動觸發一個dom事件的基本步驟
手動觸發一個dom事件,需要3步,如果你對document.createEvent,不是很熟悉,可以點選檢視。
- 建立一個事件物件 document.createEvent(event)
- 初始化事件物件 event.initEvent(type, bubbles, true)
- 分發事件 dom.dispatchEvent(event)
到這裡已經完成了前面兩步,還剩最後一步了,在來看trigger
剩下的程式碼
手動觸發dom事件最後一步
$.fn.trigger = function (event, args) {
// 對傳入的event進行處理,如果是字串或者純物件,得到一個自己建立的事件物件
// 如果傳入的已經是個經過$.Event處理的物件,則放入compatible再次改造(其實就是新增了幾個方法,和重寫了幾個方法)
event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)
// args傳遞給事件處理程式的引數
event._args = args
return this.each(function () {
// handle focus(), blur() by calling them directly
if (event.type in focus && typeof this[event.type] == "function") this[event.type]()
// items in the collection might not be DOM elements
// 觸發dom事件
else if ('dispatchEvent' in this) this.dispatchEvent(event)
// 因為zepto物件內部的元素不一定是dom元素,此時直接觸發回撥函式
else $(this).triggerHandler(event, args)
})
}複製程式碼
最後一步其實就是將當前選中的元素進行一次each遍歷,然後判斷要觸發的事件是不是focus
或者blur
,如果是就直接執行。
再進一步,如果dispatchEvent
方法在當前的dom元素屬性中存在,那麼便將該事件觸發。(為什麼要這一步呢?因為我們知道$()函式的使用方式有很多,有些方式得到的zepto物件是沒有選中dom節點的)
最後還有一個else分支,這個分支處理走的不是手動觸發事件,而是直接觸發註冊事件時新增的事件處理程式(因為這裡涉及到zepto事件模組中如何管理元素與事件佇列的對映關係,篇幅會比較長,會在接下來的文章中說,這裡不展開說明)
手動diy一個
根據上面的描述,手動觸發DOM事件,原來並沒有那麼神奇,完成三步,即可達到目標。我們自己來手動寫一個示例
<ul class="list">
<li class="item1">1</li>
<li>2</li>
<li>3</li>
</ul>複製程式碼
let $list = document.querySelector('.list')
let $item1 = document.querySelector('.item1')
$list.addEventListener('click', function () {
console.log(this.innerHTML)
}, false)
$item1.addEventListener('click', function () {
console.log(this.innerHTML)
}, false)
// 1. 建立一個事件物件 document.createEvent(event)
let event = document.createEvent('Event')
// 2. 初始化事件物件 event.initEvent(type, bubbles, true)
event.initEvent('click', true, true)
// 3. 分發事件 dom.dispatchEvent(event)
$item1.dispatchEvent(event)複製程式碼
這個時候控制檯列印出了
1
<li class="item1">1</li>
<li>2</li>
<li>3</li>複製程式碼
1是item1的事件處理函式列印出來的。
後面的li那部分則是list列印出來的。
如果將initEvent的第二個引數設定為false,將不允許冒泡,則只會列印出1
結尾
如果這部分對你有點點幫助,點個star好不好呀! ???