vue watch陣列引發的血案
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)方法去處理該元素,就返回到最先類繼續往下走;

5. 回到Observer,如果判斷需要被監聽的值不為陣列,則呼叫walk方法,處理該元素。
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