Vue資料繫結簡析
作為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
等不影響理解的邏輯及邊界條件,儘可能簡化地讓流程更加直觀:
最後,本著學習的心態,在寫作的過程中也零零碎碎的查閱了很多資料,其中難免出現紕漏以及未覆蓋到的知識點,如有錯誤,還請不吝指教。