由自定義事件到vue數據響應

前言

除了你們常常提到的自定義事件以外,瀏覽器自己也支持咱們自定義事件,咱們常說的自定義事件通常用於項目中的一些通知機制。最近正好看到了這部分,就一塊兒看了下自定義事件不一樣的實現,以及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類庫

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

  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的數據響應,網上相關文章簡直太多了,這裏就不深刻去討論了。簡單搬運一下基本概念。詳細的話你們能夠自行搜索。

基本原理

直接看圖比較直觀: 就是經過觀察者模式來實現,不過其經過數據劫持方式實現的更加巧妙。
數據劫持是經過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地址
源碼地址

相關文章
相關標籤/搜索