回顧
在前面的幾個 step
中,我們實現物件的屬性的監聽,但是有關於陣列的行為我們一直沒有處理。
我們先分析下導致陣列有哪些行為:
- 呼叫方法:
arr.splice(1, 2, 'something1', 'someting2')
- 直接賦值:
arr[1] = 'something'
解決行為一
首先我們知道陣列下的一些方法是會對原陣列照成影響的,有以下幾個:
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
這幾個方法總的來說會照成幾個影響:
- 陣列長度發生變化
- 陣列內元素順序發生變化
不像物件,如果物件的 key
值的順序發生變化,是不會影響檢視的變化,但陣列的順序如果發生變化,檢視是要變化的。
也就是說當著幾個方法觸發的時候,我們需要檢視的更新,也就是要觸發 Dep
中的 notify
函式。
但是縱觀我們現在實現的程式碼( step5
中的程式碼),我們並沒有特地的為陣列提供一個 Dep
。
並且上述的幾個陣列方法是陣列物件提供的,我們要想辦法去觸發 Dep
下的 notify
函式。
我們先為陣列提供一個 Dep
,完善後的 Observer
:
export class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
// 為陣列設定一個特殊的 Dep
this.dep = new Dep()
this.observeArray(value)
} else {
this.walk(value)
}
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true
})
}
/**
* 遍歷物件下屬性,使得屬性變成可監聽的結構
*/
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* 同上,遍歷陣列
*/
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製程式碼
同樣的在 defineReactive
我們需要處理陣列新增依賴的邏輯
export function defineReactive(object, key, value) {
let dep = new Dep()
let childOb = observe(value)
Object.defineProperty(object, key, {
configurable: true,
enumerable: true,
get: function () {
if (Dep.target) {
dep.addSub(Dep.target)
Dep.target.addDep(dep)
// 處理陣列的依賴
if(Array.isArray(value)){
childOb.dep.addSub(Dep.target)
Dep.target.addDep(childOb.dep)
}
}
return value
},
set: function (newValue) {
if (newValue !== value) {
value = newValue
dep.notify()
}
}
})
}
複製程式碼
ok 我們現在完成了依賴的新增,剩下的我們要實現依賴的觸發。
處理方法:在陣列物件呼叫特定方法時,首先找到的應該是我們自己寫的方法,而這個方法中呼叫了原始方法,並觸發依賴。
我們先來包裝一下方法,得到一些同名方法:
const arrayProto = Array.prototype
// 複製方法
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 改變陣列的預設處理,將新新增的物件新增監聽
*/
methodsToPatch.forEach(function (method) {
// 原始的陣列處理方法
const original = arrayProto[method]
let mutator = function (...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 方法
ob.dep.notify()
return result
}
Object.defineProperty(arrayMethods, method, {
value: mutator,
enumerable: false,
writable: true,
configurable: true
})
})
複製程式碼
ok 我們現在得到了一些列同名的方法,我只要確保在呼叫時,先呼叫到我們的方法即可。
有兩種方式可以實現:
- 陣列物件上直接有該方法,這樣就不會去找物件上的原型鏈
- 覆蓋物件的
__proto__
,這樣尋找原型鏈時,就會先找到我們的方法
具體到程式碼中的實現:
export class Observer {
constructor(value) {
this.value = value
if (Array.isArray(value)) {
this.dep = new Dep()
const augment = ('__proto__' in {})
? protoAugment
: copyAugment
// 覆蓋陣列中一些改變了原陣列的方法,使得方法得以監聽
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
...
}
...
}
/**
* 如果能使用 __proto__ 則將陣列的處理方法進行替換
*/
function protoAugment (target, src, keys) {
target.__proto__ = src
}
/**
* 如果不能使用 __proto__ 則直接將該方法定義在當前物件下
*/
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
Object.defineProperty(target, key, {
value: src[key],
enumerable: false,
writable: true,
configurable: true
})
}
}
複製程式碼
測試一下:
let object = {
arrayTest: [1, 2, 3, 4, 5]
}
observe(object)
let watcher = new Watcher(object, function () {
return this.arrayTest.reduce((sum, num) => sum + num)
}, function (newValue, oldValue) {
console.log(`監聽函式,陣列內所有元素 = ${newValue}`)
})
object.arrayTest.push(10)
// 監聽函式,陣列內所有元素 = 25
複製程式碼
到現在為止,我們成功的在陣列呼叫方法的時候,新增並觸發了依賴。
解決行為二
首先先說明,陣列下的索引是和物件下的鍵有同樣的表現,也就是可以用 defineReactive
來處理索引值,但是陣列是用來存放一系列的值,我們並不能一開始就確定陣列的長度,並且極有可能剛開始陣列長度為 0
,之後陣列中的索引對應的內容也會不斷的變化,所以為索引呼叫 defineReactive
是不切實際的。
但是類似於 arr[1] = 'something'
這樣的賦值在陣列中也是常見的操作,在 Vue
中實現了 $set
具體的細節這裡不談,這裡實現了另一種方法,我們僅僅需要在陣列物件下新增一個方法即可:
arrayMethods.$apply = function () {
this.__ob__.observeArray(this)
this.__ob__.dep.notify()
}
複製程式碼
測試一下:
object.arrayTest[1] = 10
object.arrayTest.$apply()
// 監聽函式,陣列內所有元素 = 33
複製程式碼
到目前為了,一個完整的資料監聽的模型也就完成了,我們可以使用 observe
方法來得到一個可監聽結構,然後用 Watcher
新增依賴。
在設定值的時候就能成功觸發依賴。