簡單實現一個雙向繫結

Hong丶發表於2019-02-19

看了一些關於雙向繫結的文章,現在來整理一下思路。 首先實現雙向繫結有三個步驟:

  1. 需要一個方法來識別哪一個的view被繫結了相應的資料
  2. 需要監視資料和view的變化
  3. 需要將所有變化傳播到繫結的物件和對應的view

為了解決第一個問題,要在對應的dom上新增相應的data-bind-<prop_name>屬性,比如:

  num: <input type="number" data-bind-num>
  <div data-bind-num></div>
複製程式碼

為了解決第二個問題,一方面監聽資料改變,需要這樣新增一個set()方法進行監聽:

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

複製程式碼

規定通過set(key, val)的方式來修改資料。 另一邊監聽對應檢視改變就直接監聽input事件。

為了解決第三個問題就需要用釋出訂閱模式實現一個事件樞紐:

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)
}
複製程式碼

將檢視層的變化傳播到資料:

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

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

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()都可以用直接賦值代替。

改變data效果:

clipboard.png

修改input效果:

clipboard.png

文章相關程式碼已經同步到Github,歡迎查閱~

相關文章