data () {
return {
nameList: ['jiang', 'ru', 'yi']
}
},
methods: {
handleClick () {
// 通過push,unshift等方法改變陣列可以通過watch監聽到
this.nameList.push('瑤')
// 直接通過陣列下標進行修改陣列無法通過watch監聽到
this.nameList[2] = '愛'
// 通過$set修改陣列可以通過watch監聽到
this.$set(this.nameList, 2, '張')
// 利用陣列splice方法修改陣列可以通過watch監聽到
this.nameList.splice(2, 1, '蔣如意')
}
},
watch: {
nameList (newVal) {
console.log(newVal)
}
}
複製程式碼
總結
變異方法
Vue包含一組觀察陣列的變異方法,所以它們也將會觸發檢視更新,這些方法如下:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
替換陣列
變異方法,顧名思義,會改變被這些方法呼叫的原始陣列。相比之下,也有非變異方法,例如:filter(),concat()和slice()。這些不會改變原始陣列,但總是返回一個新陣列。當使用非變異方法時,可以用新陣列替換就陣列
注意事項
由於JavaScript的限制,Vue不能檢測以下變動的陣列
1.當你利用索引直接設定一個項時,例如:vm.items[index] = newValue
2.當你修改陣列的長度時,例如:vm.items.length = newLength
為了解決第一類問題,以下兩種方式可以實現
// 方法一
Vue.set(vm.items, index, newValue)
Vue.splice(index, 1, newValue)
複製程式碼
為了解決第二類問題,可以使用splice
vm.items.splice(newLength)
複製程式碼
小發現:通過下標直接更改陣列元素,無法觸發渲染機制更新檢視,但此時陣列的值已經發生變化,若同時有其他資料更改導致重新渲染時,繫結陣列的dom也會更新顯示最新的資料
通過vue表象解釋
- vue在對資料監聽時,需要資料在初始化的時候就已經確定屬性的key,通過Object.defineProperty進行資料劫持,從而實現在資料發生變化時觸發檢視更新,例如:
obj: {
name: '蔣',
age: '28'
}
複製程式碼
name和age兩個屬性從初始化的時候就已經確定了,此時更改obj中的兩個屬性值是可以被監聽到並且觸發檢視更新的; 如果通過js程式碼對obj物件新增一個新屬性,那麼當這個屬性發生變化時是無法被監聽到的,除非使用this.$set方法新增的新物件; 2. 陣列也是一個物件,索引相當於物件屬性的key值,但是vue在針對單一的陣列時,是沒有對該索引對應的值進行資料劫持的,所以直接更改陣列元素的值無法被監聽到, 並且不能觸發檢視更新,例如:
arr1: [1, 2, 3, 4];
通過arr1[0] = 666,無法被監聽到
arr2: [
{
name: 'a'
},
{
name: 'b'
}
]
arr2[0].name = 'cc';
複製程式碼
此時的更改是可以被監聽到,並且觸發檢視更新的
我的疑問:為什麼vue不對單一的陣列元素進行資料劫持呢,親測可以通過資料劫持的方式來觸發set方法
// 我的測試方式如下
------------- def開始 -----------------
function def (obj, key, val) {
var value = val
Object.defineProperty(obj, key, {
set (newVal) {
console.log('觸發set')
value = newVal
},
get () {
return value
}
})
}
-------------- def結束 ----------------
var arr = [1, 2, 3]
arr.forEach((item, index) => {
def(arr, index, item)
})
arr[0] = 11
arr[1] = 22
console.log(arr) // [11, 22, 3]
-----------------------------
var obj = {
list: ['a', 'b', 'c']
}
obj.list.forEach((item, index) => {
def(obj.list, index, item)
})
obj.list[0] = 'jiang'
obj.list[1] = 'ru'
console.log(obj.list) // ['jiang', 'ru', 'c']
複製程式碼
通過原始碼層面解釋
// 由於瀏覽器相容問題,Object.observe方法不能起到監聽資料變動,所以vue在實現的過程中自己有封裝了Observe類
- Observer 類的 constructor 方法中對需要被監聽的值進行了判斷
- 如果該值為陣列,那麼需要呼叫 observeArray 方法去處理
- observeArray 方法中主要是遍歷陣列中每個元素,並且呼叫 observe 方法去處理每個元素。
- observe 方法做的事情就是,如果該元素為簡單的字串或者數字則不做任何處理,直接return;若該元素為物件的話則呼叫
new Observer(value)
方法去處理該元素,就返回到最先類繼續往下走; 從第4步就能發現為什麼通過索引改動陣列的元素無法觸發檢視更新了 - 回到 Observer,如果判斷需要被監聽的值不為陣列,則呼叫walk方法,處理該元素
- walk 方法中呼叫
Object.keys()
方法來遍歷物件,並且呼叫defineReactive(obj, keys[i])
方法 - defineReactive 方法做的事情就是利用
Object.defineProperty()
方法去監聽物件中的每個屬性; - 在 set 方法中會去呼叫
dep.notify()
方法,該方法就是去通知watcher觸發update方法去重新渲染檢視; - 在get方法中會將該屬性新增到相關的依賴中
原始碼
// 由於瀏覽器相容問題,Object.observe
方法不能起到監聽資料變動,所以vue在實現的過程中自己有封裝了 Observe 類
- Observer類的 constructor 方法中對需要被監聽的值進行了判斷
- 如果該值為陣列,那麼需要呼叫 observeArray 方法去處理
- observeArray方法中主要是遍歷陣列中每個元素,並且呼叫observe方法去處理每個元素。
- observe方法做的事情就是,如果該元素為簡單的字串或者數字則不做任何處理,直接return;若該元素為物件的話則呼叫new Observer(value)方法去處理該元素,就返回到最先類繼續往下走;
6. walk方法中呼叫Object.keys()方法來遍歷物件,並且呼叫defineReactive(obj, keys[i])方法 7. defineReactive方法做的事情就是利用Object.defineProperty()方法去監聽物件中的每個屬性;
8. 在set方法中會去呼叫dep.notify()方法,該方法就是去通知watcher觸發update方法去重新渲染檢視;
9. 在get方法中會將該屬性新增到相關的依賴中
怎樣通過watch來監聽一個陣列
// 例一:一個簡單的陣列
data () {
return {
dataList: [1, 2, 3, 4]
}
},
methods: {
handleClick () {
this.dataList.forEach((item, index) => {
// 首先這裡通過遍歷陣列改變元素的值,不能直接進行賦值更改,否則無法被監聽到
// item = '你好'
// 需要用$set方法進行賦值
this.$set(this.dataList, index, '你好')
})
}
},
watch: {
dataList (newVal) {
console.log(newVal) // ['你好', '你好', '你好', '你好']
}
}
// 例二: 一個物件陣列
data () {
return {
dataList: [
{
label: '一年級',
status: '上課'
},
{
label: '二年級',
status: '上課'
},
{
label: '三年級',
status: '上課'
},
{
label: '四年級',
status: '上課'
},
{
label: '五年級',
status: '上課'
},
{
label: '六年級',
status: '上課'
}
]
}
},
methods: {
handleClick () {
// 如果是物件陣列,可以通過這種方法改變元素的值,並且能夠觸發檢視更行
this.dataList.forEach(item => {
item.status = '下課'
})
}
},
watch: {
// dataList (newVal) { // 無法監聽到陣列變化
// newVal.forEach(item => {
// console.log(item.status)
// })
// },
dataList: { // 通過設定deep的值可以監聽到
handler () {
newVal.forEach(item => {
console.log(item.status) // '下課', '下課', '下課', '下課', '下課', '下課'
})
},
deep: true
}
}
複製程式碼
通過上述例子可以發現:
- 對於一個單一簡單的陣列,如果需要更改裡面元素的值時,需要通過this.$set方法進行更改,此時可以被監聽到,並且觸發檢視更新
- 需要強調的一點,如果簡單的陣列不是通過this.$set方法更改的那麼不管watch中是否設定deep:true都沒有用,無法監聽到陣列發生的變化
- 通過例二可以發現,物件陣列中,每個物件中的元素可以直接進行更改並且能夠觸發檢視更新,但是如果需要通過watch來監聽這個陣列是否發生變化,則必須加上deep:true