4

看了一些关于双向绑定的文章,现在来整理一下思路。
首先实现双向绑定有三个步骤:

  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,欢迎查阅~

RThong
280 声望7 粉丝