淺析Vue響應式原理(二)
Vue響應式原理之Observer
之前簡單介紹了Dep和Watcher類的程式碼和作用,現在來介紹一下Observer類和set/get。在Vue例項後再新增響應式資料時需要藉助Vue.set/vm.$set
方法,這兩個方法內部實際上呼叫了set方法。而Observer所做的就是將修改反映到檢視中。
Observer
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that has this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { const augment = hasProto ? protoAugment : copyAugment augment(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]) } } }
Observer
有三個屬性。value
是響應式資料的值;dep
是Dep例項,這個Dep例項用於Vue.set/vm.$set
中通知依賴更新;vmCount
表示把這個資料當成根data物件的例項數量,大於0時是例項化傳入的根data物件。
建構函式接受一個值,表示要觀察的值,這樣,在Observer例項中引用了響應式資料,並將響應式資料的__ob__
屬性指向自身。如果被觀察值是除陣列以外的型別,會呼叫walk
方法,令每個屬性都是響應式。對於基本型別的值,Object.keys
會返回一個空陣列,所以在walk
內,defineReactive
只在物件的屬性上執行。如果是被觀察值是陣列,那麼會在每個元素上呼叫工廠函式observe
對於陣列,響應式的實現稍有不同。回顧一下在教程陣列更新檢測裡的說明,變異方法會觸發檢視更新。其具體實現就在這裡。arrayMethods
是一個物件,儲存了Vue重寫的陣列方法,具體重寫方式下面再說,現在只需知道這些重寫的陣列方法除了保持原陣列方法的功能外,還能通知依賴資料已更新。augment
的用途是令value
能夠呼叫在arrayMethods
中的方法,實現的方式有兩種。第一種是通過原型鏈實現,在value.__proto__
新增這些方法,優先選擇這種實現。部分瀏覽器不支援__proto__
,則直接在value
上新增這些方法。
最後執行observeArray
方法,遍歷value
observe
方法。
陣列變異方法的實現
執行變異方法會觸發檢視功能,所以變異方法要實現的功能,除了包括原來陣列方法的功能外,還要有通知依賴資料更新的功能。程式碼儲存在/src/core/observer/array.js
。
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]
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
})
})
模組內,使用arrayProto
儲存陣列原型,arrayMethods
的原型是arrayProto
,用來儲存變異後的方法,methodsToPatch
是儲存變異方法名的陣列。
遍歷methodsToPatch
,根據方法名來獲取在arrayProto
上的陣列變異方法,然後在arrayMethods
實現同名方法。
在該同名方法內,首先執行快取的陣列方法original
,執行上下文是this
,這些方法最終會新增到響應式陣列或其原型上,所以被呼叫時this
是陣列本身。ob
指向this.__ob__
,使用inserted
指向被插入的元素,呼叫ob.observeArray
觀察新增的陣列元素。最後執行ob.dep.notify()
,通知依賴更新。
observe
工廠函式,獲取value上__ob__
屬性指向的Observer例項,如果需要該屬性且未定義時,根據資料建立一個Observer例項,在例項化時會在value上新增__ob__
屬性。引數二表示傳入的value
是否是根data物件。只有根資料物件的__ob__.vmCount
大於0。
isObject
判斷value
是不是Object型別,實現如obj !== null && typeof obj === 'object'
。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
此處可以看出,value
與Observer例項ob
之間是雙向引用。value.__ob__
指向ob
,ob.value
指向value
。
Vue.set
在Vue例項化以後,如果想為其新增新的響應式屬性,對於物件,直接使用字面量賦值是沒有效果的。由響應式資料的實現可以想到,這種直接賦值的方式,並沒有為該屬性自定義getter/setter,在獲取屬性時不會收集依賴,在更新屬性時不會觸發更新。如果想要為已存在的響應式資料新增新屬性,可以使用Vue.set/vm.$set
方法,但要注意,不能在data上新增新屬性。
Vue.set/vm.$set
內部都是在/src/code/observer/index.js
定義的set
的函式。
set
函式接受三個引數,引數一target
表示要新增屬性的物件,引數二key
表示新增的屬性名或索引,引數三val
表示新增屬性的初始值。
export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
!Array.isArray(target) &&
!isObject(target)
) {
warn(`Cannot set reactive property on non-object/array value: ${target}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 不存在ob 說明不是響應式資料
if (!ob) {
target[key] = val
return val
}
// 為target新增新屬性
defineReactive(ob.value, key, val)
// ob.dep實際是target.__ob__.dep
ob.dep.notify()
return val
}
函式內部首先判斷target
型別,非陣列或非物件的目標資料是無法新增響應式資料的。
如果是陣列,且key
是有效的陣列索引,更新陣列長度,然後呼叫變異方法splice
,更新對應的值並觸發檢視更新。如果是物件,且屬性key
在target
的原型鏈上且不在Object.prototype
上(即不是Object原型上定義的屬性或方法),直接在target
上新增或更新key
。
令ob
指向target.__ob__
,如果target
是Vue例項或是根data物件(ob.vmCount > 0
),則無法新增資料,直接返回。
接著處理能為target
新增屬性的情況。不存在ob時,說明不是響應式資料,直接更新target
。否則,執行defineReactive
函式為ob.value
新增響應式屬性,ob.value
實際指向target
,新增之後呼叫ob.dep.notify()
通知觀察者重新求值,ob是Observer例項。
總結一下,set的內部邏輯:
當target
是陣列時,更新長度,呼叫變異方法splice
插入新元素即可。
當target
是物件時:
-
key
在除Object.prototype
外的原型鏈上時,直接賦值 -
key
在原型鏈上搜索不到時,需要新增屬性。如果target
無__ob__
屬性,說明不是響應式資料,直接賦值。否則呼叫defineReactive(ob.value, key, val)
觀察新資料,同時觸發依賴。
Vue.delete
刪除物件的屬性。如果物件是響應式的,確保刪除能觸發更新檢視。
Vue.delete
實際指向del
。del
接受兩個引數,引數一target
表示要刪除屬性的物件,引數二key
表示要刪除的屬性名。
如果target
是陣列且key
對於的索引在target
中存在,使用變異方法splice
方法直接刪除。
如果target
是Vue例項或是根data物件則返回,不允許在其上刪除屬性。key
不是例項自身屬性時也返回,不允許刪除。如果是自身屬性則使用delete
刪除,接著判斷是否有__ob__
屬性,如果有,說明是響應式資料,執行__ob__.dep.notify
通知檢視更新。
export function del (target: Array<any> | Object, key: any) {
if (process.env.NODE_ENV !== 'production' &&
!Array.isArray(target) &&
!isObject(target)
) {
warn(`Cannot delete reactive property on non-object/array value: ${target}`)
}
// 陣列 直接刪除元素
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
// 屬性不在target上
if (!hasOwn(target, key)) {
return
}
delete target[key]
// 不是響應式資料
if (!ob) {
return
}
ob.dep.notify()
}
小結
關於Observer類和set/get的原始碼已經做了簡單的分析,細心的讀者可能會有一個問題:target.__ob__.dep
是什麼時候收集依賴的。答案就在defineReactive
的原始碼中,其收集操作同樣在響應式資料的getter中執行。
至於defineReactive
的原始碼解析,在後面的文章再做分析。