Vue雙向繫結初探

sako發表於2019-04-09

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 })

相關文章