由自定義事件到vue數據響應
前言
除了大家經常提到的自定義事件之外,瀏覽器本身也支持我們自定義事件,我們常說的自定義事件一般用於項目中的一些通知機制。最近正好看到了這部分,就一起看了下自定義事件不同的實現,以及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地址
源碼地址
由自定義事件到vue數據響應