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