簡單實現一個雙向綁定

看了一些關於雙向綁定的文章,如今來整理一下思路。
首先實現雙向綁定有三個步驟:html

  1. 須要一個方法來識別哪個的view被綁定了相應的數據
  2. 須要監視數據和view的變化
  3. 須要將全部變化傳播到綁定的對象和對應的view

爲了解決第一個問題,要在對應的dom上添加相應的data-bind-<prop_name>屬性,好比:vue

num: <input type="number" data-bind-num>
  <div data-bind-num></div>

爲了解決第二個問題,一方面監聽數據改變,須要這樣添加一個set()方法進行監聽:git

const Vue = {
  data: {
    num: 0
  },
  set(key, val) {
    this.data[key] = val
  }
}

規定經過set(key, val)的方式來修改數據。
另外一邊監聽對應視圖改變就直接監聽input事件。github

爲了解決第三個問題就須要用發佈訂閱模式實現一個事件樞紐:dom

const EventHub = {
  callbacks: {},

  on(eventName, callback){
    this.callbacks[eventName] = this.callbacks[eventName] || [];
    this.callbacks[eventName].push(callback);
  },

  emit(eventName, ...rest){
    this.callbacks[eventName] = this.callbacks[eventName] || [];
    for(let i = 0; i < this.callbacks[eventName].length; i++){
      this.callbacks[eventName][i].call(this,...rest);
    }
  }
}

一方面將數據層的變化傳播到視圖,須要用特定名稱與dom上的屬性對應:優化

//觸發事件就修改視圖
eventHub.on('num:change', (val) => {
  $(`input[data-bind-num]`).val(val)
  $(`div[data-bind-num]`).text(val)
})
//經過set()修改data來觸發對應的change事件
set(key, val) {
  this.data[key] = val
  EventHub.emit('num:change', val)
}

將視圖層的變化傳播到數據:this

$(`input[data-bind-num]`).on('input', function() {
  let val = $(this).val() === '' ? 0 : parseInt($(this).val())
  Vue.set(key, val)
})

至此雙向綁定就實現完成!可是這樣一個個寫事件名和屬性名有點蠢,優化一下spa

const fn = (prop_name) => {     
  return {
    dataBind: `data-bind-${prop_name}`,//對應dom的data屬性名
    eventName: `${prop_name}:change`//對應數據的change事件名稱
  }      
}

//給全部data綁定change事件,給全部data對應的view綁定input事件
Object.keys(Vue.data).map((key) => {
  //data修改改變view
  EventHub.on(fn(key).eventName, (val) => {

    $(`input[${fn(key).dataBind}]`).val(val)
    $(`div[${fn(key).dataBind}]`).text(val)

  })

  //view改變data
  $(`input[${fn(key).dataBind}]`).on('input', function() {

    let val = $(this).val() === '' ? '' : parseInt($(this).val())
    Vue.set(key, val)

  })
})

這樣實現的雙向綁定依賴於用set()來改變數據,而咱們都但願經過 vm.property = value 這種方式直接來修改數據,這就須要用到Object.defineProperty()來劫持各個數據的getter,setter雙向綁定

//給各個數據添加監聽器,用數據劫持替換原先的set(key,value)
const Observer = {
  mapProp(obj) {
    if(!obj || typeof obj !== 'object') {
      return
    }
    Object.keys(obj).map((key) => {
      this.defineReactive(obj, key, obj[key])
    })
  },
  defineReactive(obj, key, val) {
    this.mapProp(val)
    Object.defineProperty(obj, key, {
      enumerable: true, // 可枚舉
      configurable: false, // 不能再define
      get() {
        return val
      },
      set(newVal) {
        console.log(`數據 ${key} 從${val}->${newVal}`)
        //當數據變化就貴觸發對應的change事件
        EventHub.emit(fn(key).eventName, newVal)
        val = newVal
      }
    })
  }
}

這樣只須要調用一次Observer.mapProp(Vue.data)就會監聽全部data,原先的set()均可以用直接賦值代替。rest

改變data效果:

clipboard.png

修改input效果:

clipboard.png

文章相關代碼已經同步到 Github,歡迎查閱~
相關文章
相關標籤/搜索