由自定義事件到vue資料響應

瀟湘待雨發表於2018-08-26

前言

除了大家經常提到的自定義事件之外,瀏覽器本身也支援我們自定義事件,我們常說的自定義事件一般用於專案中的一些通知機制。最近正好看到了這部分,就一起看了下自定義事件不同的實現,以及vue資料響應的基本原理。

瀏覽器自定義事件

定義

除了我們常見的click,touch等事件之外,瀏覽器支援我們定義和分發自定義事件。 建立也十分簡單:

//建立名為test的自定義事件
var event = new Event('test')
//如果是需要更多引數可以這樣
var event = new CustomEvent('test', { 'detail': elem.dataset.time });
複製程式碼

大多數現代瀏覽器對new Event/CustomEvent 的支援還算可以(IE除外),可以看下具體情況: 由自定義事件到vue資料響應 可以放心大膽的使用,如果非要相容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地址由自定義事件到vue資料響應

大概思路捋一下:

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三個方法。分別看一下

  1. 對照原生事件很容易理解,繫結一個事件,應該有對應方法名和回撥,當然還有一個事件佇列
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)
      }
    }
}
複製程式碼
  1. 觸發事件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);
        }
      }
    }
複製程式碼
  1. 解除繫結:
// 取消繫結,還是迴圈查詢
    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的資料響應,網上相關文章簡直太多了,這裡就不深入去討論了。簡單搬運一下基本概念。詳細的話大家可以自行搜尋。

基本原理

直接看圖比較直觀: 由自定義事件到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地址
原始碼地址

相關文章