Vue 依賴收集原理分析
Vue實例在初始化時讼呢,可以接受以下幾類數(shù)據(jù):
- 模板
- 初始化數(shù)據(jù)
- 傳遞給組件的屬性值
- computed
- watch
- methods
Vue 根據(jù)實例化時接受的數(shù)據(jù)撩鹿,在將數(shù)據(jù)和模板轉化成DOM節(jié)點的同時,分析其依賴的數(shù)據(jù)吝岭。在特定數(shù)據(jù)改變時三痰,自動在下一個周期重新渲染DOM節(jié)點
本文主要分析Vue是如何進行依賴收集的吧寺。
Vue中窜管,與依賴收集相關的類有:
Dep : 一個訂閱者的列表類,可以增加或刪除訂閱者稚机,可以向訂閱者發(fā)送消息
Watcher : 訂閱者類幕帆。它在初始化時可以接受getter
, callback
兩個函數(shù)作為參數(shù)。getter
用來計算Watcher對象的值赖条。當Watcher被觸發(fā)時失乾,會重新通過getter
計算當前Watcher的值常熙,如果值改變,則會執(zhí)行callback
.
對初始化數(shù)據(jù)的處理
對于一個Vue組件碱茁,需要一個初始化數(shù)據(jù)的生成函數(shù)裸卫。如下:
export default {
data () {
return {
text: 'some texts',
arr: [],
obj: {}
}
}
}
Vue為數(shù)據(jù)中的每一個key維護一個訂閱者列表。對于生成的數(shù)據(jù)纽竣,通過Object.defineProperty
對其中的每一個key進行處理墓贿,主要是為每一個key設置get
, set
方法,以此來為對應的key收集訂閱者蜓氨,并在值改變時通知對應的訂閱者聋袋。部分代碼如下:
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
每一key都有一個訂閱者列表
const dep = new Dep()
在為key進行賦值時,如果值發(fā)生了改變穴吹,則會通知所有的訂閱者
dep.notify()
在對key進行取值時幽勒,如果Dep.target
有值,除正常的取值操作外會進行一些額外的操作來添加訂閱者港令。大多數(shù)時間里啥容,Dep.target
的值都為null
,只有訂閱者在進行訂閱操作時缠借,Dep.target
才有值干毅,為正在進行訂閱的訂閱者。此時進行取值操作泼返,會將訂閱者加入到對應的訂閱者列表中硝逢。
訂閱者在進行訂閱操作時,主要包含以下3個步驟:
- 將自己放在
Dep.target
上 - 對自己依賴的key進行取值
- 將自己從
Dep.target
移除
在執(zhí)行訂閱操作后绅喉,訂閱者會被加入到相關key的訂閱者列表中渠鸽。
針對對象和數(shù)組的處理
如果為key賦的值為對象:
- 會遞歸地對這個對象中的每一key進行處理
如果為key賦的值為數(shù)組:
- 遞歸地對這個數(shù)組中的每一個對象進行處理
- 重新定義數(shù)組的
push
,pop
,shift
,unshift
,splice
,sort
,reverse
方法,調用以上方法時key的訂閱者列表會通知訂閱者們“值已改變”柴罐。如果調用的是push
,unshift
,splice
方法徽缚,遞歸處理新增加的項
對模板的處理
Vue將模板處理成一個render
函數(shù)。需要重新渲染DOM時革屠,render
函數(shù)結合Vue實例中的數(shù)據(jù)生成一個虛擬節(jié)點凿试。新的虛擬節(jié)點和原虛擬節(jié)點進行對比,對需要修改的DOM節(jié)點進行修改似芝。
訂閱者
訂閱者在初始化時主要接受2個參數(shù)getter
, callback
那婉。getter
用來計算訂閱者的值,所以其在執(zhí)行時會對訂閱者所有需要訂閱的key進行取值党瓮。訂閱者的訂閱操作主要是通過getter
來實現(xiàn)详炬。
部分代碼如下:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
主要步驟:
- 將自己放在
Dep.target
上(pushTarget(this)
) - 執(zhí)行
getter
(this.getter.call(vm, vm)
) - 將自己從
Dep.target
移除(popTarget()
) - 清理之前的訂閱(
this.cleanupDeps()
)
此后,訂閱者在依賴的key的值發(fā)生變化會得到通知寞奸。獲得通知的訂閱者并不會立即被觸發(fā)呛谜,而是會被加入到一個待觸發(fā)的數(shù)組中在跳,在下一個周期統(tǒng)一被觸發(fā)。
訂閱者在被觸發(fā)時隐岛,會執(zhí)行getter
來計算訂閱者的值猫妙,如果值改變,則會執(zhí)行callback
.
負責渲染DOM的訂閱者
Vue實例化后都會生成一個用于渲染DOM的訂閱者聚凹。此訂閱者在實例化時傳入的getter
方法為渲染DOM的方法吐咳。
部分代碼如下:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
vm._watcher = new Watcher(vm, updateComponent, noop)
vm._render()
結合模板和數(shù)據(jù),計算出虛擬DOM
vm._update()
根據(jù)虛擬DOM渲染真實的DOM節(jié)點
此訂閱者在初始化時就會進行訂閱操作元践。實例化時傳入的getter
為updateComponent
韭脊。其中的vm._render()
在執(zhí)行時一定會對所有依賴的key進行取值,能完成對依賴的key的訂閱单旁。同時vm._update()
完成了第一次DOM渲染沪羔。當前依賴的key的值發(fā)生變化,訂閱者被觸發(fā)時象浑,作為getter
的updateComponent
會重新執(zhí)行蔫饰,重新渲染DOM。因為getter
返回的值一直為undefined
,所以此訂閱者中的callback
并沒有被用到愉豺,于是傳入了一個空函數(shù)noop
作為callback
對computed的處理
通過computed可以定義一組計算屬性篓吁,通過計算屬性可以將一些復雜的計算過程抽離出來,保持模板的簡單和清晰蚪拦。
代碼示例:
export default {
data () {
return {
text: 'some texts',
arr: [],
obj: {}
}
},
computed: {
key1: function () {
return this.text + this.arr.length
}
}
}
在定義一個計算屬性時杖剪,需要定義一個key和一個計算方法。
Vue在對computed進行處理時驰贷,會為每一個計算屬性生成一個lazy狀態(tài)的訂閱者盛嘿。普通的訂閱者在實例化和觸發(fā)時會執(zhí)行getter
來計算自身的值和進行訂閱操作。而lazy狀態(tài)的訂閱者在上述情況下只會將自身置為dirty狀態(tài)括袒,不進行其它操作次兆。在訂閱者執(zhí)行自身的evaluate
方法時,會清除自身的dirty狀態(tài)并執(zhí)行getter
來計算自身的值和進行訂閱锹锰。
Vue在為計算屬性生成訂閱者時的示例代碼如下:
const computedWatcherOptions = { lazy: true }
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)
傳入的getter
為自定義的計算方法芥炭,callback
為空函數(shù)。(lazy狀態(tài)的訂閱者永遠都沒有機會執(zhí)行callback
)
Vue 在自身實例上為指定key定義get
方法恃慧,使可以通過Vue實例獲取計算屬性的值园蝠。
部分代碼如下:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
在對計算屬性定義的key進行取值時,會首先獲取之前生成好的訂閱者糕伐。只有訂閱者處于dirty狀態(tài)時砰琢,才會執(zhí)行evaluate
計算訂閱者的值蘸嘶。所以為計算屬性定義的計算方法只有在對計算屬性的key進行取值并且計算屬性依賴的key曾經(jīng)改變時才會執(zhí)行良瞧。
假如對上文定義的計算屬性key1
進行取值
vm.key1; //第一次取值陪汽,自定義計算方法執(zhí)行
vm.key1; //第二次取值,依賴的key的值沒有變化褥蚯,自定義計算方法不會執(zhí)行
vm.text = '' //改變計算屬性依賴的key的值挚冤,計算屬性對應的訂閱者會進入dirty狀態(tài),自定義計算方法不會執(zhí)行
vm.key1; //第三次取值赞庶,計算屬性依賴的key的值發(fā)生了變化并且對計算屬性進行取值训挡,自定義的計算方法執(zhí)行
訂閱計算屬性值的變化
計算屬性的key不會維護一個訂閱者列表,也不能通過計算屬性的set
方法在觸發(fā)所有訂閱者歧强。(計算屬性不能被賦值)澜薄。一個訂閱者執(zhí)行訂閱操作來訂閱計算屬性值的變化其實是訂閱了計算屬性依賴的key的值的變化。
在計算屬性的get
方法中
if (Dep.target) {
watcher.depend()
}
如果有訂閱者來訂閱計算屬性的變化摊册,計算屬性會將自己的訂閱復制到正在進行訂閱的訂閱者上肤京。watcher.depend()
的作用就是如此。
例如:
//初始化訂閱者watcher, 依賴計算屬性key1
var watcher = new Watcher(function () {
return vm.key1
}, noop)
vm.text = '' //計算屬性key1依賴的text的值發(fā)生變化茅特,watcher會被觸發(fā)
對watch的處理
Vue實例化時可以傳入watch對象忘分,來監(jiān)聽某些值的變化。
例如:
export default {
watch: {
'a.b.c': function (val, oldVal) {
console.log(val)
console.log(oldVal)
}
}
}
Vue 會為watch中的每一項生成一個訂閱者白修。訂閱者的getter
通過處理字符串得到妒峦。如'a.b.c'會被處理成
function (vm) {
var a = vm.a
var b = a.b
var c = b.c
return c
}
處理字符串的源碼如下:
/**
* Parse simple path.
*/
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
訂閱者的callback
為定義watch時傳入的監(jiān)聽函數(shù)。當訂閱者被觸發(fā)時兵睛,如果訂閱者的值發(fā)生變化肯骇,則會執(zhí)行callback
。callback
執(zhí)行時會傳入變化后的值祖很,變化前的值作為參數(shù)累盗。