除了你們常常提到的自定義事件以外,瀏覽器自己也支持咱們自定義事件,咱們常說的自定義事件通常用於項目中的一些通知機制。最近正好看到了這部分,就一塊兒看了下自定義事件不一樣的實現,以及vue數據響應的基本原理。javascript
除了咱們常見的click,touch等事件以外,瀏覽器支持咱們定義和分發自定義事件。 建立也十分簡單:html
//建立名爲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);
複製代碼
自定義事件的觸發和原生事件相似,能夠經過冒泡事件觸發。vue
<form>
<textarea></textarea>
</form>
複製代碼
觸發以下,這裏就偷個懶,直接拿mdn的源碼來示例了,畢竟清晰易懂。java
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));
複製代碼
上面例子很清晰的展現了自定義事件定義、監聽、觸發的整個過程,和原生事件的流程相比看起來多了個觸發的步驟,緣由在原生事件的觸發已經被封裝無需手動處理而已。react
各類js庫中用到的也比較多,例如zepto中的tap,原理就是監聽touch事件,而後去觸發自定的tap事件(固然這種成熟的框架作的是比較嚴謹的)。能夠看下部分代碼:git
//這裏作了個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結構比較簡單github
<div >一本書的價格:<input type='text' id='el' value=10 /></div>
<div >5本書的價格:<span id='el2'>50</span>元</div>
複製代碼
當改變input值得時候,效果以下demo地址 : 數組
大概思路捋一下:瀏覽器
一、自定義事件,priceChange,用來監聽改變price的改變
二、 加個監聽事件,priceChange觸發時改變total的值。
三、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(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的數據響應,網上相關文章簡直太多了,這裏就不深刻去討論了。簡單搬運一下基本概念。詳細的話你們能夠自行搜索。
直接看圖比較直觀: 就是經過觀察者模式來實現,不過其經過數據劫持方式實現的更加巧妙。
數據劫持是經過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地址
源碼地址