vue watch陣列引發的血案

空白2017發表於2018-10-25
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表象解釋

  1. 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類

  1. Observer 類的 constructor 方法中對需要被監聽的值進行了判斷
  2. 如果該值為陣列,那麼需要呼叫 observeArray 方法去處理
  3. observeArray 方法中主要是遍歷陣列中每個元素,並且呼叫 observe 方法去處理每個元素。
  4. observe 方法做的事情就是,如果該元素為簡單的字串或者數字則不做任何處理,直接return;若該元素為物件的話則呼叫 new Observer(value) 方法去處理該元素,就返回到最先類繼續往下走; 從第4步就能發現為什麼通過索引改動陣列的元素無法觸發檢視更新了
  5. 回到 Observer,如果判斷需要被監聽的值不為陣列,則呼叫walk方法,處理該元素
  6. walk 方法中呼叫Object.keys()方法來遍歷物件,並且呼叫 defineReactive(obj, keys[i])方法
  7. defineReactive 方法做的事情就是利用Object.defineProperty()方法去監聽物件中的每個屬性;
  8. 在 set 方法中會去呼叫dep.notify()方法,該方法就是去通知watcher觸發update方法去重新渲染檢視;
  9. 在get方法中會將該屬性新增到相關的依賴中

原始碼

// 由於瀏覽器相容問題,Object.observe 方法不能起到監聽資料變動,所以vue在實現的過程中自己有封裝了 Observe 類

  1. Observer類的 constructor 方法中對需要被監聽的值進行了判斷
  2. 如果該值為陣列,那麼需要呼叫 observeArray 方法去處理
    vue watch陣列引發的血案
  3. observeArray方法中主要是遍歷陣列中每個元素,並且呼叫observe方法去處理每個元素。
  4. observe方法做的事情就是,如果該元素為簡單的字串或者數字則不做任何處理,直接return;若該元素為物件的話則呼叫new Observer(value)方法去處理該元素,就返回到最先類繼續往下走;

vue watch陣列引發的血案
5. 回到Observer,如果判斷需要被監聽的值不為陣列,則呼叫walk方法,處理該元素。
6. walk方法中呼叫Object.keys()方法來遍歷物件,並且呼叫defineReactive(obj, keys[i])方法

vue watch陣列引發的血案
7. defineReactive方法做的事情就是利用Object.defineProperty()方法去監聽物件中的每個屬性;
8. 在set方法中會去呼叫dep.notify()方法,該方法就是去通知watcher觸發update方法去重新渲染檢視;
9. 在get方法中會將該屬性新增到相關的依賴中

vue watch陣列引發的血案

vue watch陣列引發的血案

怎樣通過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
    }
}

複製程式碼

通過上述例子可以發現:

  1. 對於一個單一簡單的陣列,如果需要更改裡面元素的值時,需要通過this.$set方法進行更改,此時可以被監聽到,並且觸發檢視更新
  2. 需要強調的一點,如果簡單的陣列不是通過this.$set方法更改的那麼不管watch中是否設定deep:true都沒有用,無法監聽到陣列發生的變化
  3. 通過例二可以發現,物件陣列中,每個物件中的元素可以直接進行更改並且能夠觸發檢視更新,但是如果需要通過watch來監聽這個陣列是否發生變化,則必須加上deep:true

相關文章