Vue 的雙向數據綁定,使得修改數據后,視圖就會跟着發生更新,比如對數組進行增加元素、切割等操作。然而直接通過下標修改數組內容后,視圖卻不發生變化。那么,在保留原有的數組響應方式下,為什么 Vue 不增加對數組下標的響應式監聽呢?
arr[index] = val 不是響應式的
在 Vue 官網的 列表渲染 — Vue.js 中,有強調 Vue 不能 直接檢測通過數組下標改變值的變化,需要通過 數組更新檢測 來實現。
<template> <div> <span v-for="i in arr">{{ i }}</span> <button @click="updateIndex">改變下標對應的值</button> <span v-for="key in Object.keys(obj)">{{ obj[key] }}</span> <button @click="updateKey">改變key對應的值</button> </div> </template> <script> export default { data() { return { arr: [ 1, 2, 3, 4 ], obj: { a: 1, b: 2, c: 3, d: 4 } } }, methods: { updateIndex() { this.arr[0]++ // 對數組這樣的操作不會引起視圖的更新 // this.arr.splice(0, 0) // 需要調用數組的方法,才能使視圖更新 }, updateKey() { this.obj['a']++ // 但對對象這樣會引起視圖更新 } } } </script>
從源碼看 Vue 中數組的 Observer 實現
在 Vue 2.6.10 中,可以看到 Observer (/src/core/observer/index.js) 的實現方式:
export class Observer { // ...... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { // 這里對數組進行單獨處理 if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { // 對對象遍歷所有鍵值 this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
可以看到 vue 對對象是采取 Object.keys
然后 defineReactive
所有鍵值,而對數組並沒這樣做,而是只 observe
了每個元素的值,數組的下標因為沒有被監聽,所以直接通過下標修改值是不會更新視圖的。
而數組方法能夠響應式,是因為 Vue 對數組的方法進行了 def
操作 (/src/core/observer/array.js)
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) })
並非不能實現下標響應式
但數組也是對象的一種,它的下標就是它的鍵,只是平常使用時,數組的鍵數量往往比對象的鍵數量大的多。所以原則上它也是可以使用對象的處理方式。通過修改 源碼 后引入后查看效果:
export class Observer { // .... constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) this.walk(value) // 保留原有的數組監聽方式下,增加對下標的監聽響應 } else { this.walk(value) } } // ...... }
視圖代碼還是和上面的一樣,點擊按鈕可以看到視圖會實時更新:
實驗探測數組下標響應式對性能的影響
通過上面的修改,可以知道 Vue 其實是可以監聽數組下標的。但為什么 Vue 不采取,且說是“JavaScript的限制”呢?在 github issue#8562 中,Vue.js 作者尤雨溪解釋是因為性能問題。
為了驗證數組下標響應式對性能的影響,我做了以下實驗實現相同的效果,分別設置循環次數 TIMES 為 1000,10000,100000(以下只貼出關鍵代碼部分,其他部分代碼一致):
- 使用修改能響應下標觸發頁面更新的 Vue ,通過數組下標修改值 TIMES 次
<template> <div> <span v-for="i in arr">{{ i }}</span> <button @click="updateIndex">改變下標對應的值</button> </div> </template> <script> // import modified vue export default { data() { return { arr: new Array(100).fill(0) } }, methods: { updateIndex() { console.time('updateIndex') for (let i = 0; i < TIMES; i++) { this.arr[0]++ } console.timeEnd('updateIndex') } } } </script>
- 使用原版 vue 通過數組下標修改值 TIMES 次,並通過 splice 方法觸發視圖更新
<template><!-- 和上面一樣 --></template> <script> // import origin vue export default { data() { /* 和上面一樣 */ }, methods: { updateIndex() { console.time('updateIndex') for (let i = 0; i < TIMES; i++) { this.arr[0]++ } this.arr.splice(0, 0) // 通過 splice 實現視圖更新 console.timeEnd('updateIndex') } } } </script>
每個實驗不同 TIMES 都重復10次,取平均值,實驗數據如下:
增加數組下標響應式對性能會有影響
通過上面的實驗,可見在循環次數較少的時候,增加下標響應式似乎沒有多大影響,但隨循環次數增加,帶來的性能損耗將快速增加。如果想要實現直接修改下標對應的內容來自動更新視圖,對性能會有一些影響。因此對於數組的更新,最好還是通過數組更新檢測來實現。
在選擇 TIMES 取值的時候,也發現需要到 10000 級別才會體現出較明顯的差距。但一般情況下,我們並不會執行像上面一樣龐大的操作,也許僅僅只是改變一個值而已,實現下標響應式消耗的時間和普通的方式幾乎一樣,或許在這方面 vue 犧牲了一點開發體驗。