我們知道通過Object.defineProperty()劫持數組為其設置getter和setter后,調用的數組的push、splice、pop等方法改變數組元素時並不會觸發數組的setter,這就會造成使用上述方法改變數組后,頁面上並不能及時體現這些變化,也就是數組數據變化不是響應式的(對上述不了解的可以參考這篇文章)。但實際用vue開發時,對於響應式數組,使用push、splice、pop等方法改變數組時,頁面會及時體現這種變化,那么vue中是如何實現的呢?
通過vue源碼可以看出,vue重寫了數組的push、splice、pop等方法。
1 // src/core/observer/array.js 2 3 // 獲取數組的原型Array.prototype,上面有我們常用的數組方法 4 const arrayProto = Array.prototype 5 // 創建一個空對象arrayMethods,並將arrayMethods的原型指向Array.prototype 6 export const arrayMethods = Object.create(arrayProto) 7 8 // 列出需要重寫的數組方法名 9 const methodsToPatch = [ 10 'push', 11 'pop', 12 'shift', 13 'unshift', 14 'splice', 15 'sort', 16 'reverse' 17 ] 18 // 遍歷上述數組方法名,依次將上述重寫后的數組方法添加到arrayMethods對象上 19 methodsToPatch.forEach(function (method) { 20 // 保存一份當前的方法名對應的數組原始方法 21 const original = arrayProto[method] 22 // 將重寫后的方法定義到arrayMethods對象上,function mutator() {}就是重寫后的方法 23 def(arrayMethods, method, function mutator (...args) { 24 // 調用數組原始方法,並傳入參數args,並將執行結果賦給result 25 const result = original.apply(this, args) 26 // 當數組調用重寫后的方法時,this指向該數組,當該數組為響應式時,就可以獲取到其__ob__屬性 27 const ob = this.__ob__ 28 let inserted 29 switch (method) { 30 case 'push': 31 case 'unshift': 32 inserted = args 33 break 34 case 'splice': 35 inserted = args.slice(2) 36 break 37 } 38 if (inserted) ob.observeArray(inserted) 39 // 將當前數組的變更通知給其訂閱者 40 ob.dep.notify() 41 // 最后返回執行結果result 42 return result 43 }) 44 })
從上面可以看出array.js中重寫了數組的push、pop、shift、unshift、splice、sort、reverse七種方法,重寫方法在實現時除了將數組方法名對應的原始方法調用一遍並將執行結果返回外,還通過執行ob.dep.notify()將當前數組的變更通知給其訂閱者,這樣當使用重寫后方法改變數組后,數組訂閱者會將這邊變化更新到頁面中。
重寫完數組的上述7種方法外,我們還需要將這些重寫的方法應用到數組上,因此在Observer構造函數中,可以看到在監聽數據時會判斷數據類型是否為數組。當為數組時,如果瀏覽器支持__proto__,則直接將當前數據的原型__proto__指向重寫后的數組方法對象arrayMethods,如果瀏覽器不支持__proto__,則直接將arrayMethods上重寫的方法直接定義到當前數據對象上;當數據類型為非數組時,繼續遞歸執行數據的監聽。
1 // src/core/observer/index.js 2 export class Observer { 3 ... 4 constructor (value: any) { 5 this.value = value 6 this.dep = new Dep() 7 this.vmCount = 0 8 def(value, '__ob__', this) 9 if (Array.isArray(value)) { 10 if (hasProto) { 11 protoAugment(value, arrayMethods) 12 } else { 13 copyAugment(value, arrayMethods, arrayKeys) 14 } 15 this.observeArray(value) 16 } else { 17 this.walk(value) 18 } 19 } 20 ... 21 } 22 function protoAugment (target, src: Object) { 23 /* eslint-disable no-proto */ 24 target.__proto__ = src 25 /* eslint-enable no-proto */ 26 } 27 function copyAugment (target: Object, src: Object, keys: Array<string>) { 28 for (let i = 0, l = keys.length; i < l; i++) { 29 const key = keys[i] 30 def(target, key, src[key]) 31 } 32 }
經過上述處理后,對於數組,當我們調用其方法處理數組時會按照如下原型鏈來獲取數組方法:
對於響應式數組,當瀏覽器支持__proto__屬性時,使用push等方法時先從其原型arrayMethods上尋找push方法,也就是重寫后的方法,處理之后數組的變化會通知到其訂閱者,更新頁面,當在arrayMethods上查詢不到時會向上在Array.prototype上查詢;當瀏覽器不支持__proto__屬性時,使用push等方法時會先從數組自身上查詢,如果查詢不到會向上再Array.prototype上查詢。
對於非響應式數組,當使用push等方法時會直接從Array.prototype上查詢。
值得一提的是源碼中通過判斷瀏覽器是否支持__proto__來分別使用protoAugment和copyAugment 方法將重寫后的數組方法應用到數組中,這是因為對於IE10及以下的IE瀏覽器是不支持__proto__屬性的:
上述截圖參考於Vue源碼解析五——數據響應系統
結論:
在將數組處理成響應式數據后,如果使用數組原始方法改變數組時,數組值會發生變化,但是並不會觸發數組的setter來通知所有依賴該數組的地方進行更新,為此,vue通過重寫數組的某些方法來監聽數組變化,重寫后的方法中會手動觸發通知該數組的所有依賴進行更新。