前言
除了大家經常提到的自定義事件之外,瀏覽器本身也支援我們自定義事件,我們常說的自定義事件一般用於專案中的一些通知機制。最近正好看到了這部分,就一起看了下自定義事件不同的實現,以及vue資料響應的基本原理。
瀏覽器自定義事件
定義
除了我們常見的click,touch等事件之外,瀏覽器支援我們定義和分發自定義事件。 建立也十分簡單:
//建立名為test的自定義事件
var event = new Event('test')
//如果是需要更多引數可以這樣
var event = new CustomEvent('test', { 'detail': elem.dataset.time });
複製程式碼
大多數現代瀏覽器對new Event/CustomEvent 的支援還算可以(IE除外),可以看下具體情況: 可以放心大膽的使用,如果非要相容IE那麼有下面的方式
var event = document.createEvent('Event');
//相關引數
event.initEvent('test', true, true);
複製程式碼
自定義事件的觸發和原生事件類似,可以通過冒泡事件觸發。
<form>
<textarea></textarea>
</form>
複製程式碼
觸發如下,這裡就偷個懶,直接拿mdn的原始碼來示例了,畢竟清晰易懂。
const form = document.querySelector('form');
const textarea = document.querySelector('textarea');
//建立新的事件,允許冒泡,支援傳遞在details中定義的所有資料
const eventAwesome = new CustomEvent('awesome', {
bubbles: true,
detail: { text: () => textarea.value }
});
//form元素監聽自定義的awesome事件,列印text事件的輸出
// 也就是text的輸出內容
form.addEventListener('awesome', e => console.log(e.detail.text()));
//
// textarea當輸入時,觸發awesome
textarea.addEventListener('input', e => e.target.dispatchEvent(eventAwesome));
複製程式碼
上面例子很清晰的展示了自定義事件定義、監聽、觸發的整個過程,和原生事件的流程相比看起來多了個觸發的步驟,原因在原生事件的觸發已經被封裝無需手動處理而已。
應用
各大js類庫
各種js庫中用到的也比較多,例如zepto中的tap,原理就是監聽touch事件,然後去觸發自定的tap事件(當然這種成熟的框架做的是比較嚴謹的)。可以看下部分程式碼:
//這裡做了個event的map,來將原始事件對應為自定義事件以便處理
// 可以只關注下ontouchstart,這裡先判斷是否移動端,移動端down就對應touchstart,up對應touchend,後面的可以先不關注
eventMap = (__eventMap && ('down' in __eventMap)) ? __eventMap :
('ontouchstart' in document ?
{ 'down': 'touchstart', 'up': 'touchend',
'move': 'touchmove', 'cancel': 'touchcancel' } :
'onpointerdown' in document ?
{ 'down': 'pointerdown', 'up': 'pointerup',
'move': 'pointermove', 'cancel': 'pointercancel' } :
'onmspointerdown' in document ?
{ 'down': 'MSPointerDown', 'up': 'MSPointerUp',
'move': 'MSPointerMove', 'cancel': 'MSPointerCancel' } : false)
//監聽事件
$(document).on(eventMap.up, up)
.on(eventMap.down, down)
.on(eventMap.move, move)
//up事件即touchend時,滿足條件的會觸發tap
var up = function (e) {
/* 忽略 */
tapTimeout = setTimeout(function () {
var event = $.Event('tap')
event.cancelTouch = cancelAll
if (touch.el) touch.el.trigger(event);
},0)
}
//其他
複製程式碼
釋出訂閱
和原生事件一樣,大部分都用於觀察者模式中。除了上面的庫之外,自己開發過程中用到的地方也不少。
舉個例子,一個輸入框表示單價,另一個div表示五本的總價,單價改變總價也會變動。藉助自定義事件應該怎麼實現呢。
html結構比較簡單
<div >一本書的價格:<input type='text' id='el' value=10 /></div>
<div >5本書的價格:<span id='el2'>50</span>元</div>
複製程式碼
當改變input值得時候,效果如下demo地址 :
大概思路捋一下:
1、自定義事件,priceChange,用來監聽改變price的改變
2、 加個監聽事件,priceChange觸發時改變total的值。
3、input value改變的時候,觸發priceChange事件
程式碼實現如下:
const count = document.querySelector('#el'),
total1 = document.querySelector('#el2');
const eventAwesome = new CustomEvent('priceChange', {
bubbles: true,
detail: { getprice: () => count.value }
});
document.addEventListener('priceChange', function (e) {
var price = e.detail.getprice() || 0
total1.innerHTML=5 * price
})
el.addEventListener('change', function (e) {
var val = e.target.value
e.target.dispatchEvent(eventAwesome)
});
複製程式碼
程式碼確實比較簡單,當然實現的方式是多樣的。但是看起來是不是有點vue資料響應的味道。
確實目前大多數框架中都會用到釋出訂閱的方式來處理資料的變化。例如vue,react等,以vue為例子,我們可以來看看其資料響應的基本原理。
自定義事件
這裡的自定義事件就是前面提到的第二層定義了,非基於瀏覽器的事件。這種事件也正是大型前端專案中常用到。對照原生事件,應該具有on、trigger、off三個方法。分別看一下
- 對照原生事件很容易理解,繫結一個事件,應該有對應方法名和回撥,當然還有一個事件佇列
class Event1{
constructor(){
// 事件佇列
this._events = {}
}
// type對應事件名稱,call回撥
on(type,call){
let funs = this._events[type]
// 首次直接賦值,同種型別事件可能多個回撥所以陣列
// 否則push進入佇列即可
if(funs){
funs.push(call)
}else{
this._events.type=[]
this._events.type.push(call)
}
}
}
複製程式碼
- 觸發事件trigger
// 觸發事件
trigger(type){
let funs = this._events.type,
[first,...other] = Array.from(arguments)
//對應事件型別存在,迴圈執行回撥佇列
if(funs){
let i = 0,
j = funs.length;
for (i=0; i < j; i++) {
let cb = funs[i];
cb.apply(this, other);
}
}
}
複製程式碼
- 解除繫結:
// 取消繫結,還是迴圈查詢
off(type,func){
let funs = this._events.type
if(funs){
let i = 0,
j = funs.length;
for (i = 0; i < j; i++) {
let cb = funs[i];
if (cb === func) {
funs.splice(i, 1);
return;
}
}
}
return this
}
}
複製程式碼
這樣一個簡單的事件系統就完成了,結合這個事件系統,我們可以實現下上面那個例子。
html不變,繫結和觸發事件的方式改變一下就好
// 初始化 event1為了區別原生Event
const event1 = new Event1()
// 此處監聽 priceChange 即可
event1.on('priceChange', function (e) {
// 值獲取方式修改
var price = count.value || 0
total1.innerHTML = 5 * price
})
el.addEventListener('change', function (e) {
var val = e.target.value
// 觸發事件
event1.trigger('priceChange')
});
複製程式碼
這樣同樣可以實現上面的效果,實現了事件系統之後,我們接著實現一下vue裡面的資料響應。
vue的資料響應
說到vue的資料響應,網上相關文章簡直太多了,這裡就不深入去討論了。簡單搬運一下基本概念。詳細的話大家可以自行搜尋。
基本原理
直接看圖比較直觀:
就是通過觀察者模式來實現,不過其通過資料劫持方式實現的更加巧妙。
資料劫持是通過Object.defineProperty()來監聽各個屬性的變化,從而進行一些額外操作。
舉個簡單例子:
let a = {
b:'1'
}
Object.defineProperty(a,'b',{
get(){
console.log('get>>>',1)
return 1
},
set(newVal){
console.log('set>>>11','設定是不被允許的')
return 1
}
})
a.b //'get>>>1'
a.b = 11 //set>>>11 設定是不被允許的
複製程式碼
所謂資料劫持就是在get/set操作時加上額外操作,這裡是加了些log,如果在這裡去監聽某些屬性的變化,進而更改其他屬性也是可行的。
要達到目的,應該對每個屬性在get是監聽,set的時候出發事件,且每個屬性上只註冊一次。
另外應該每個屬性對應一個監聽者,這樣處理起來比較方便,如果和上面那樣全放在一個監聽例項裡面,有多個屬性及複雜操作時,就太難維護了。
//基本資料
let data = {
price: 5,
count: 2
},
callb = null
複製程式碼
可以對自定義事件進行部分改造,
不需要顯式指定type,全域性維護一個標記即可
事件陣列一維即可,因為是每個屬性對應一個示例
class Events {
constructor() {
this._events = []
}
on() {
//此處不需要指定tyep了
if (callb && !this._events.includes(callb)) {
this._events.push(callb)
}
}
triger() {
this._events.forEach((callb) => {
callb && callb()
})
}
}
複製程式碼
對應上圖中vue的Data部分,就是實行資料劫持的地方
Object.keys(data).forEach((key) => {
let initVlue = data[key]
const e1 = new Events()
Object.defineProperty(data, key, {
get() {
//內部判斷是否需要註冊
e1.on()
// 執行過置否
callb = null
// get不變更值
return initVlue
},
set(newVal) {
initVlue = newVal
// set操作觸發事件,同步資料變動
e1.triger()
}
})
})
複製程式碼
此時資料劫持即事件監聽準備完成,大家可能會發現callback始終為null,這始終不能起作用。為了解決該問題,下面的watcher就要出場了。
function watcher(func) {
// 引數賦予callback,執行時觸發get方法,進行監聽事件註冊
callb = func
// 初次執行時,獲取對應值自然經過get方法註冊事件
callb()
// 置否避免重複註冊
callb = null
}
// 此處指定事件觸發回撥,註冊監聽事件
watcher(() => {
data.total = data.price * data.count
})
複製程式碼
這樣就保證了會將監聽事件掛載上去。到這裡,乞丐版資料響應應該就能跑了。
再加上dom事件的處理,雙向繫結也不難實現。
可以將下面的完整程式碼放到console臺跑跑看。
let data = {
price: 5,
count: 2
},
callb = null
class Events {
constructor() {
this._events = []
}
on() {
if (callb && !this._events.includes(callb)) {
this._events.push(callb)
}
}
triger() {
this._events.forEach((callb) => {
callb && callb()
})
}
}
Object.keys(data).forEach((key) => {
let initVlue = data[key]
const e1 = new Events()
Object.defineProperty(data, key, {
get() {
//內部判斷是否需要註冊
e1.on()
// 執行過置否
callb = null
// get不變更值
return initVlue
},
set(newVal) {
initVlue = newVal
// set操作觸發事件,同步資料變動
e1.triger()
}
})
})
function watcher(func) {
// 引數賦予callback,執行時觸發get方法,進行監聽事件註冊
callb = func
// 初次執行時,獲取對應值自然經過get方法註冊事件
callb()
// 置否避免重複註冊
callb = null
}
// 此處指定事件觸發回撥,註冊監聽事件
watcher(() => {
data.total = data.price * data.count
})
複製程式碼
結束語
參考文章
vue資料響應的實現
Creating and triggering events
看到知識盲點,就需要立即行動,不然下次還是盲點。正好是事件相關,就一併總結了下發布訂閱相關進而到了資料響應的實現。個人的一點心得記錄,分享出來希望共同學習和進步。更多請移步我的部落格
demo地址
原始碼地址