作為MVVM框架的一種,Vue最為人津津樂道的當是資料與檢視的繫結,將直接操作DOM節點變為修改data
資料,利用Virtual Dom
來Diff
對比新舊檢視,從而實現更新。不僅如此,還可以通過Vue.prototype.$watch
來監聽data
的變化並執行回撥函式,實現自定義的邏輯。雖然日常的編碼運用已經駕輕就熟,但未曾去深究技術背後的實現原理。作為一個好學的程式設計師,知其然更要知其所以然,本文將從原始碼的角度來對Vue響應式資料中的觀察者模式進行簡析。
初始化Vue
例項
在閱讀原始碼時,因為檔案繁多,引用複雜往往使我們不容易抓住重點,這裡我們需要找到一個入口檔案,從Vue
建構函式開始,拋開其他無關因素,一步步理解響應式資料的實現原理。首先我們找到Vue
建構函式:
// src/core/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
複製程式碼
// src/core/instance/init.js
Vue.prototype._init = function (options) {
...
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 初始化vm例項的$options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
initLifecycle(vm) // 梳理例項的parent、root、children和refs,並初始化一些與生命週期相關的例項屬性
initEvents(vm) // 初始化例項的listeners
initRender(vm) // 初始化插槽,繫結createElement函式的vm例項
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 掛載元件到節點
}
}
複製程式碼
為了方便閱讀,我們去除了flow
型別檢查和部分無關程式碼。可以看到,在例項化Vue元件時,會呼叫Vue.prototype._init
,而在方法內部,資料的初始化操作主要在initState
(這裡的initInjections
和initProvide
與initProps
類似,在理解了initState
原理後自然明白),因此我們重點來關注initState
。
// src/core/instance/state.js
export function initState (vm) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製程式碼
首先初始化了一個_watchers
陣列,用來存放watcher
,之後根據例項的vm.$options
,相繼呼叫initProps
、initMethods
、initData
、initComputed
和initWatch
方法。
initProps
function initProps (vm, propsOptions) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
...
defineReactive(props, key, value)
if (!(key in vm)) {
proxy(vm, '_props', key)
}
}
toggleObserving(true)
}
複製程式碼
在這裡,vm.$options.propsData
是通過父元件傳給子元件例項的資料物件,如<my-element :item="false"></my-element>
中的{item: false}
,然後初始化vm._props
和vm.$options._propKeys
分別用來儲存例項的props
資料和keys
,因為子元件中使用的是通過proxy
引用的_props
裡的資料,而不是父元件傳遞的propsData
,所以這裡快取了_propKeys
,用來updateChildComponent
時能更新vm._props
。接著根據isRoot
是否是根元件來判斷是否需要呼叫toggleObserving(false)
,這是一個全域性的開關,來控制是否需要給物件新增__ob__
屬性。這個相信大家都不陌生,一般的元件的data
等資料都包含這個屬性,這裡先不深究,等之後和defineReactive
時一起講解。因為props
是通過父傳給子的資料,在父元素initState
時已經把__ob__
新增上了,所以在不是例項化根元件時關閉了這個全域性開關,待呼叫結束前在通過toggleObserving(true)
開啟。
之後是一個for
迴圈,根據元件中定義的propsOptions
物件來設定vm._props
,這裡的propsOptions
就是我們常寫的
export default {
...
props: {
item: {
type: Object,
default: () => ({})
}
}
}
複製程式碼
迴圈體內,首先
const value = validateProp(key, propsOptions, propsData, vm)
複製程式碼
validateProp
方法主要是校驗資料是否符合我們定義的type
,以及在propsData
裡未找到key
時,獲取預設值並在物件上定義__ob__
,最後返回相應的值,在這裡不做展開。
這裡我們先跳過defineReactive
,看最後
if (!(key in vm)) {
proxy(vm, '_props', key)
}
複製程式碼
其中proxy
方法:
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製程式碼
在vm
不存在key
屬性時,通過Object.defineProperty
使得我們能通過vm[key]
訪問到vm._props[key]
。
defineReactive
在initProps
中,我們瞭解到其首先根據使用者定義的vm.$options.props
物件,通過對父元件設定的傳值物件vm.$options.propsData
進行資料校驗,返回有效值並儲存到vm._props
,同時儲存相應的key
到vm.$options._propKeys
以便進行子元件的props
資料更新,最後利用getter/setter
存取器屬性,將vm[key]
指向對vm._props[key]
的操作。但其中跳過了最重要的defineReactive
,現在我們將通過閱讀defineReactive
原始碼,瞭解響應式資料背後的實現原理。
// src/core/observer/index.js
export function defineReactive (
obj,
key,
val,
customSetter,
shallow
) {
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
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
...
}
複製程式碼
首先const dep = new Dep()
例項化了一個dep
,在這裡利用閉包來定義一個依賴項,用以與特定的key
相對應。因為其通過Object.defineProperty
重寫target[key]
的getter/setter
來實現資料的響應式,因此需要先判斷物件key
的configurable
屬性。接著
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
複製程式碼
arguments.length === 2
意味著呼叫defineReactive
時未傳遞val
值,此時val
為undefined
,而!getter || setter
判斷條件則表示如果在property
存在getter
且不存在setter
的情況下,不會獲取key
的資料物件,此時val
為undefined
,之後呼叫observe
時將不對其進行深度觀察。正如之後的setter
訪問器中的:
if (getter && !setter) return
複製程式碼
此時資料將是隻讀狀態,既然是隻讀狀態,則不存在資料修改問題,繼而無須深度觀察資料以便在資料變化時呼叫觀察者註冊的方法。
Observe
在defineReactive
裡,我們先獲取了target[key]
的descriptor
,並快取了對應的getter
和setter
,之後根據判斷選擇是否獲取target[key]
對應的val
,接著是
let childOb = !shallow && observe(val)
複製程式碼
根據shallow
標誌來確定是否呼叫observe
,我們來看下observe
函式:
// src/core/observer/index.js
export function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob
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
}
複製程式碼
首先判斷需要觀察的資料是否為物件以便通過Object.defineProperty
定義__ob__
屬性,同時需要value
不屬於VNode
的例項(VNode
例項通過Diff
補丁演算法來實現例項對比並更新)。接著判斷value
是否已有__ob__
,如果沒有則進行後續判斷:
shouldObserve
:全域性開關標誌,通過toggleObserving
來修改。!isServerRendering()
:判斷是否服務端渲染。(Array.isArray(value) || isPlainObject(value))
:陣列和純物件時才允許新增__ob__
進行觀察。Object.isExtensible(value)
:判斷value
是否可擴充套件。!value._isVue
:避免Vue
例項被觀察。
滿足以上五個條件時,才會呼叫ob = new Observer(value)
,接下來我們要看下Observer
類裡做了哪些工作
// src/core/observer/index.js
export class Observer {
constructor (value) {
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 through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
複製程式碼
建構函式裡初始化了value
、dep
和vmCount
三個屬性,為this.value
新增__ob__
物件並指向自己,即value.__ob__.value === value
,這樣就可以通過value
或__ob__
物件取到dep
和value
。vmCount
的作用主要是用來區分是否為Vue
例項的根data
,dep
的作用這裡先不介紹,待與getter/setter
裡的dep
一起解釋。
接著根據value
是陣列還是純物件來分別呼叫相應的方法,對value
進行遞迴操作。當value
為純物件時,呼叫walk
方法,遞迴呼叫defineReactive
。當value
是陣列型別時,首先判斷是否有__proto__
,有就使用__proto__
實現原型鏈繼承,否則用Object.defineProperty
實現拷貝繼承。其中繼承的基類arrayMethods
來自src/core/observer/array.js
:
// 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) {
// 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
})
})
複製程式碼
這裡為什麼要對陣列的例項方法進行重寫呢?程式碼裡的methodsToPatch
這些方法並不會返回新的陣列,導致無法觸發setter
,因而不會呼叫觀察者的方法。所以重寫了這些變異方法,使得在呼叫的時候,利用observeArray
對新插入的陣列元素新增__ob__
,並能夠通過ob.dep.notify
手動通知對應的被觀察者執行註冊的方法,實現陣列元素的響應式。
if (asRootData && ob) {
ob.vmCount++
}
複製程式碼
最後新增這個if
判斷,在Vue
例項的根data
物件上,執行ob.vmCount++
,這裡主要為了後面根據ob.vmCount
來區分是否為根資料,從而在其上執行Vue.set
和Vue.delete
。
getter/setter
在對val
進行遞迴操作後(假如需要的話),將obj[key]
的資料物件封裝成了一個被觀察者,使得能夠被觀察者觀察,並在需要的時候呼叫觀察者的方法。這裡通過Object.defineProperty
重寫了obj[key]
的訪問器屬性,對getter/setter
操作做了攔截處理,defineReactive
剩餘的程式碼具體如下:
...
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) {
...
childOb = !shallow && observe(newVal)
dep.notify()
}
})
複製程式碼
首先在getter
呼叫時,判斷Dep.target
是否存在,若存在則呼叫dep.depend
。我們先不深究Dep.target
,只當它是一個觀察者,比如我們常用的某個計算屬性,呼叫dep.depend
會將dep
當做計算屬性的依賴項存入其依賴列表,並把這個計算屬性註冊到這個dep
。這裡為什麼需要互相引用呢?這是因為一個target[key]
可以充當多個觀察者的依賴項,同時一個觀察者可以有多個依賴項,他們之間屬於多對多的關係。這樣當某個依賴項改變時,我們可以根據dep
裡維護的觀察者,呼叫他們的註冊方法。現在我們回過頭來看Dep
:
// src/core/observer/dep.js
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
複製程式碼
建構函式裡,首先新增一個自增的uid
用以做dep
例項的唯一性標誌,接著初始化一個觀察者列表subs
,並定義了新增觀察者方法addSub
和移除觀察者方法removeSub
。可以看到其在getter
中呼叫的depend
會將當前這個dep
例項新增到觀察者的依賴項,在setter
裡呼叫的notify
會執行各個觀察者註冊的update
方法,Dep.target.addDep
這個方法將在之後的Watcher
裡進行解釋。簡單來說就是會在key
的getter
觸發時進行dep
依賴收集到watcher
並將Dep.target
新增到當前dep
的觀察者列表,這樣在key
的setter
觸發時,能夠通過觀察者列表,執行觀察者的update
方法。
當然,在getter
中還有如下幾行程式碼:
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
複製程式碼
這裡可能會有疑惑,既然已經呼叫了dep.depend
,為什麼還要呼叫childOb.dep.depend
?兩個dep
之間又有什麼關係呢?
其實這兩個dep
的分工是不同的。對於資料的增、刪,利用childOb.dep.notify
來呼叫觀察者方法,而對於資料的修改,則使用的dep.notify
,這是因為setter
訪問器無法監聽到物件資料的新增和刪除。舉個例子:
const data = {
arr: [{
value: 1
}],
}
data.a = 1; // 無法觸發setter
data.arr[1] = {value: 2}; // 無法觸發setter
data.arr.push({value: 3}); // 無法觸發setter
data.arr = [{value: 4}]; // 可以觸發setter
複製程式碼
還記得Observer
建構函式裡針對陣列型別value
的響應式轉換嗎?通過重寫value
原型鏈,使得對於新插入的資料:
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
複製程式碼
將其轉換為響應式資料,並通過ob.dep.notify
來呼叫觀察者的方法,而這裡的觀察者列表就是通過上述的childOb.dep.depend
來收集的。同樣的,為了實現物件新增資料的響應式,我們需要提供相應的hack
方法,而這就是我們常用的Vue.set/Vue.delete
。
// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
...
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
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
複製程式碼
- 判斷
value
是否為陣列,如果是,直接呼叫已經hack
過的splice
即可。 - 是否已存在
key
,有的話說明已經是響應式了,直接修改即可。 - 接著判斷
target.__ob__
是否存在,如果沒有說明該物件無須深度觀察,設定返回當前的值。 - 最後,通過
defineReactive
來設定新增的key
,並呼叫ob.dep.notify
通知到觀察者。
現在我們瞭解了childOb.dep.depend()
是為了將當前watcher
收集到childOb.dep
,以便在增、刪資料時能通知到watcher
。而在childOb.dep.depend()
之後還有:
if (Array.isArray(value)) {
dependArray(value)
}
複製程式碼
/**
* Collect dependencies on array elements when the array is touched, since
* we cannot intercept array element access like property getters.
*/
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
複製程式碼
在觸發target[key]
的getter
時,如果value
的型別為陣列,則遞迴將其每個元素都呼叫__ob__.dep.depend
,這是因為無法攔截陣列元素的getter
,所以將當前watcher
收集到陣列下的所有__ob__.dep
,這樣當其中一個元素觸發增、刪操作時能通知到觀察者。比如:
const data = {
list: [[{value: 0}]],
};
data.list[0].push({value: 1});
複製程式碼
這樣在data.list[0].__ob__.notify
時,才能通知到watcher
。
target[key]
的getter
主要作用:
- 將
Dep.target
收集到閉包中dep
的觀察者列表,以便在target[key]
的setter
修改資料時通知觀察者 - 根據情況對資料進行遍歷新增
__ob__
,將Dep.target
收集到childOb.dep
的觀察者列表,以便在增加/刪除資料時能通知到觀察者 - 通過
dependArray
將陣列型的value
遞迴進行觀察者收集,在陣列元素髮生增、刪、改時能通知到觀察者
target[key]
的setter
主要作用是對新資料進行觀察,並通過閉包儲存到childOb
變數供getter
使用,同時呼叫dep.notify
通知觀察者,在此就不再展開。
Watcher
在前面的篇幅中,我們主要介紹了defineReactive
來定義響應式資料:通過閉包儲存dep
和childOb
,在getter
時來進行觀察者的收集,使得在資料修改時能觸發dep.notify
或childOb.dep.notify
來呼叫觀察者的方法進行更新。但具體是如何進行watcher
收集的卻未做過多解釋,現在我們將通過閱讀Watcher
來了解觀察者背後的邏輯。
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
...
}
}
複製程式碼
這是Vue
計算屬性的初始化操作,去掉了一部分不影響的程式碼。首先初始化物件vm._computedWatchers
用以儲存所有的計算屬性,isSSR
用以判斷是否為服務端渲染。再根據我們編寫的computed
鍵值對迴圈遍歷,如果不是服務端渲染,則為每個計算屬性例項化一個Watcher
,並以鍵值對的形式儲存到vm._computedWatchers
物件,接下來我們主要看下Watcher
這個類。
Watcher
的建構函式
建構函式接受5個引數,其中當前Vue
例項vm
、求值表示式expOrFn
(支援Function
或者String
,計算屬性中一般為Function
),回撥函式cb
這三個為必傳引數。設定this.vm = vm
用以後續繫結this.getter
的執行環境,並將this
推入vm._watchers
(vm._watchers
用以維護例項vm
中所有的觀察者),另外根據是否為渲染觀察者來賦值vm._watcher = this
(常用的render
即為渲染觀察者)。接著根據options
進行一系列的初始化操作。其中有幾個屬性:
this.lazy
:設定是否懶求值,這樣能保證有多個被觀察者發生變化時,能只呼叫求值一次。this.dirty
:配合this.lazy
,用以標記當前觀察者是否需要重新求值。this.deps
、this.newDeps
、this.depIds
、this.newDepIds
:用以維護被觀察物件的列表。this.getter
:求值函式。this.value
:求值函式返回的值,即為計算屬性中的值。
Watcher
的求值
因為計算屬性是惰性求值,所以我們繼續看initComputed
迴圈體:
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
複製程式碼
defineComputed
主要將userDef
轉化為getter/setter
訪問器,並通過Object.defineProperty
將key
設定到vm
上,使得我們能通過this[key]
直接訪問到計算屬性。接下來我們主要看下userDef
轉為getter
中的createComputedGetter
函式:
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
,在getter
觸發時,首先通過this._computedWatchers[key]
獲取到之前儲存的watcher
,如果watcher.dirty
為true
時呼叫watcher.evaluate
(執行this.get()
求值操作,並將當前watcher
的dirty
標記為false
),我們主要看下get
操作:
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
...
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
複製程式碼
可以看到,求值時先執行pushTarget(this)
,通過查閱src/core/observer/dep.js
,我們可以看到:
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
複製程式碼
pushTarget
主要是把watcher
例項進棧,並賦值給Dep.target
,而popTarget
則相反,把watcher
例項出棧,並將棧頂賦值給Dep.target
。Dep.target
這個我們之前在getter
裡見到過,其實就是當前正在求值的觀察者。這裡在求值前將Dep.target
設定為watcher
,使得在求值過程中獲取資料時觸發getter
訪問器,從而呼叫dep.depend
,繼而執行watcher
的addDep
操作:
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
複製程式碼
先判斷newDepIds
是否包含dep.id
,沒有則說明尚未新增過這個dep
,此時將dep
和dep.id
分別加到newDepIds
和newDeps
。如果depIds
不包含dep.id
,則說明之前未新增過此dep
,因為是雙向新增的(將dep
新增到watcher
的同時也需要將watcher
收集到dep
),所以需要呼叫dep.addSub
,將當前watcher
新增到新的dep
的觀察者佇列。
if (this.deep) {
traverse(value)
}
複製程式碼
再接著根據this.deep
來呼叫traverse
。traverse
的作用主要是遞迴遍歷觸發value
的getter
,呼叫所有元素的dep.depend()
並過濾重複收集的dep
。最後呼叫popTarget()
將當前watcher
移出棧,並執行cleanupDeps
:
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
...
}
複製程式碼
遍歷this.deps
,如果在newDepIds
中不存在dep.id
,則說明新的依賴裡不包含當前dep
,需要到dep
的觀察者列表裡去移除當前這個watcher
,之後便是depIds
和newDepIds
、deps
和newDeps
的值交換,並清空newDepIds
和newDeps
。到此完成了對watcher
的求值操作,同時更新了新的依賴,最後返回value
即可。
回到createComputedGetter
接著看:
if (Dep.target) {
watcher.depend()
}
複製程式碼
當執行計算屬性的getter
時,有可能表示式中還有別的計算屬性依賴,此時我們需要執行watcher.depend
將當前watcher
的deps
新增到Dep.target
即可。最後返回求得的watcher.value
即可。
總的來說我們從this[key]
觸發watcher
的get
函式,將當前watcher
入棧,通過求值表示式將所需要的依賴dep
收集到newDepIds
和newDeps
,並將watcher
新增到對應dep
的觀察者列表,最後清除無效dep
並返回求值結果,這樣就完成了依賴關係的收集。
Watcher
的更新
以上我們瞭解了watcher
的依賴收集和dep
的觀察者收集的基本原理,接下來我們瞭解下dep
的資料更新時如何通知watcher
進行update
操作。
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
複製程式碼
首先在dep.notify
時,我們將this.subs
拷貝出來,防止在watcher
的get
時候subs
發生更新,之後呼叫update
方法:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
複製程式碼
- 如果是
lazy
,則將其標記為this.dirty = true
,使得在this[key]
的getter
觸發時進行watcher.evaluate
呼叫計算。 - 如果是
sync
同步操作,則執行this.run
,呼叫this.get
求值和執行回撥函式cb
。 - 否則執行
queueWatcher
,選擇合適的位置,將watcher
加入到佇列去執行即可,因為和響應式資料無關,故不再展開。
小結
因為篇幅有限,只對資料繫結的基本原理做了基本的介紹,在這畫了一張簡單的流程圖來幫助理解Vue
的響應式資料,其中省略了一些VNode
等不影響理解的邏輯及邊界條件,儘可能簡化地讓流程更加直觀:
最後,本著學習的心態,在寫作的過程中也零零碎碎的查閱了很多資料,其中難免出現紕漏以及未覆蓋到的知識點,如有錯誤,還請不吝指教。