Vue双向绑定初探

359 阅读3分钟

vue 双向绑定初探

起源

用vue有一段时间了,在使用的过程中发现了两个有意思问题:

1、对数组的限制,检测不到下面两个行为

  • 利用索引直接设置一个项
  • 修改数组的长度
var vm = new Vue({
    data: {
        items: ['1', '2', '3']
    }
})
vm.items[1] = 'a' // 不是响应性的
vm.items.length = 5 // 不是响应性的

2、对对象属性的限制,不能检测对象属性的添加或者删除

var vm = new Vue({
    data: {
        person: {
            name: 'jon'
        }
    }
})
// `vm.person.name` 现在是响应式的

vm.person.age = '18'
// `vm.person.age` 不是响应式的

那么为了弄清楚这两个问题,首先从双向绑定的原理出发。

入双向绑定

  • Object.defineProperty vue的双向绑定与angularjs的不同,angularjs是使用脏检查来实现的,而vue则是基于Object.defineProperty这个方法来实现数据劫持,从而达到绑定的。 Object.defineProperty():
     var obj = {}
     Object.defineProperty(obj, 'name', {
         get: function() {
             console.log('get被调用了')
         },
         set: function() {
             console.log('set被调用了')
         }
     })
     obj.name = '11' // set被调用了
     console.log(obj.name) // get被调用了

知识补充 Object.defineProperty是ES5新加的特性,而且无法被shim(指把一个库引入一个旧的浏览器, 然后用旧的API, 实现一些新的API的功能),因此vue不支持IE8以下版本的浏览器。

  • 一个极简双向绑定的例子
<input type="text" id="js-input">
    <span id="js-span"></span>
    <script>
        var obj = {}
        Object.defineProperty(obj,'name',{
            set: function(newValue){
                document.getElementById('js-span').innerHTML = newValue
            }
        })
        document.getElementById('js-input').addEventListener('keyup',function(e){
            obj.name = e.target.value
        })
    </script>

当在js-input框里输入值的时候,js-span的值也会一起被修改,那么一个最简单的双向绑定就实现了,当然vue的双向绑定,肯定不是这么简单,这个demo是为了说明数据劫持的原理~

  • Object.defineProperty的限制 现在来看下这个vue是怎么利用Object.defineProperty的
var vm = new Vue({
    el: '#demo',
    data: {
        count: 1 //基础类型数据
    }
})
//实际上在构造函数的时候就完成了数据的绑定。如果你在创建实例后又为该对象属性重置了一次,则会根据自己定义的方法,也就是set属性来执行,所以当执行vm.count=2时候,会执行console.log('change')

Object.defineProperty(vm, 'count', {

    set: function(newvalue) {
        console.log('change');
        //此处的newvalue就是新的值,可以做视图更新操作,前提在于第二个参数vm.count是已知的属性
        this.value = newvalue;
    },

    get: function() {
        return this.value
    }
})

把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter 由此可以看出,因为动态添加的属性,没有被转为getter/setter,就引发了最开始的问题2。 现在来研究下数组的问题:

var a = [0, 1, 2]
a.length = 10
// 只是显示的给length赋值,索引3-9的对应的value也会赋值undefined
// 但是索引3-9的key都是没有值的
// 我们可以用for-in打印,只会打印0,1,2
for (var key in a) {
    console.log(key) // 0,1,2
}

由于只能循环出前面三个,所以只将前三个值转化为了getter/setter,数组的下标和长度都没有被转化为 getter/setter,因此引发了第一个问题,但是vue重写了数组的push, pop, shift, unshift, splice, sort, reverse方法,使其可以双向绑定。 一个小尝试如果我们这么做

      var arr = [1, 2, 3]
      Object.defineProperty(arr, 'length', {
          set: function() {
              console.log('被set了')
          }
      })

那么会出现 “ Cannot redefine property: length”的错误,length是个无法被转为getter/setter的属性

解决方法

针对这两个情况,vue 给出了相关的解决方案

问题1的方案:

  • Vue.set(vm.items, indexOfItem, newValue)
  • vm.items.splice(indexOfItem, 1, newValue)
  • vm.items.splice(newLength)

问题2的方案:

  • Vue.set(vm.someObject, 'b', 2)
  • this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })