實際上不管是 $watch 方法仍是 watch 選項,他們的實現都是基於 Watcher 的封裝。首先咱們來看一下 $watch 方法,它定義在 src/core/instance/state.js 文件的 stateMixin 函數中,以下:
偵聽屬性的初始化也是發生在 Vue 的實例初始化階段的 initState 函數中,在 computed 初始化以後,執行了:vue
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }
$watch 方法容許咱們觀察數據對象的某個屬性,當屬性變化時執行回調。因此 $watch 方法至少接收兩個參數,一個要觀察的屬性,以及一個回調函數。經過上面的代碼咱們發現,$watch 方法接收三個參數,除了前面介紹的兩個參數以後還接收第三個參數,它是一個選項參數,好比是否當即執行回調或者是否深度觀測等。咱們能夠發現這三個參數與 Watcher 類的構造函數中的三個參數相匹配,以下:express
export default class Watcher { constructor ( vm: Component, > expOrFn: string | Function, > cb: Function, > options?: ?Object, isRenderWatcher?: boolean ) { // 省略... } }
由於 $watch 方法的實現本質就是建立了一個 Watcher 實例對象。另外經過官方文檔的介紹可知 $watch 方法的第二個參數既能夠是一個回調函數,也能夠是一個純對象,這個對象中能夠包含 handler 屬性,該屬性的值將做爲回調函數,同時該對象還能夠包含其餘屬性做爲選項參數,如 immediate 或 deep數組
假設傳遞給$watch 方法的第二個參數是一個函數,看看它是怎麼實現的,在 $watch 方法內部首先執行的是以下代碼:數據結構
const vm: Component = this if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) }
定義了 vm 常量,它是當前組件實例對象,接着檢測傳遞給 $watch 的第二個參數是不是純對象,因爲咱們如今假設參數 cb 是一個函數,因此這段 if 語句塊內的代碼不會執行。再往下是這段代碼:異步
options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options)
首先若是沒有傳遞 options 選項參數,那麼會給其一個默認的空對象,接着將 options.user 的值設置爲 true,咱們前面講到過這表明該觀察者實例是用戶建立的,而後就到了關鍵的一步,即建立 Watcher 實例對象,多麼簡單的實現async
再往下是一段 if 語句塊:ide
if (options.immediate) { cb.call(vm, watcher.value) }
immediate 選項用來在屬性或函數被偵聽後當即執行回調,如上代碼就是其實現原理,若是發現 options.immediate 選項爲真,那麼會執行回調函數,不過此時回調函數的參數只有新值沒有舊值。同時取值的方式是經過前面建立的觀察者實例對象的 watcher.value 屬性。咱們知道觀察者實例對象的 value 屬性,保存着被觀察屬性的值。函數
最後 $watch 方法還有一個返回值,以下:工具
return function unwatchFn () { watcher.teardown() }
$watch 函數返回一個函數,這個函數的執行會解除當前觀察者對屬性的觀察。它的原理是經過調用觀察者實例對象的 watcher.teardown 函數實現的。咱們能夠看一下 watcher.teardown 函數是如何解除觀察者與屬性之間的關係的,以下是 teardown 函數的代碼:性能
export default class Watcher { // 省略... /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
首先檢查 this.active 屬性是否爲真,若是爲假則說明該觀察者已經不處於激活狀態,什麼都不須要作,若是爲真則會執行 if 語句塊內的代碼,在 if 語句塊內首先執行的這段代碼:
if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) }
每一個組件實例都有一個 vm._isBeingDestroyed 屬性,它是一個標識,爲真說明該組件實例已經被銷燬了,爲假說明該組件尚未被銷燬,因此以上代碼的意思是若是組件沒有被銷燬,那麼將當前觀察者實例從組件實例對象的 vm._watchers 數組中移除,咱們知道 vm._watchers 數組中包含了該組件全部的觀察者實例對象,因此將當前觀察者實例對象從 vm._watchers 數組中移除是解除屬性與觀察者實例對象之間關係的第一步。因爲這個參數的性能開銷比較大,因此僅在組件沒有被銷燬的狀況下才會執行此操做。
將觀察者實例對象從 vm._watchers 數組中移除以後,會執行以下這段代碼:
let i = this.deps.length while (i--) { this.deps[i].removeSub(this) }
咱們知道當一個屬性與一個觀察者創建聯繫以後,屬性的 Dep 實例對象會收集到該觀察者對象,同時觀察者對象也會將該 Dep 實例對象收集,這是一個雙向的過程,而且一個觀察者能夠同時觀察多個屬性,這些屬性的 Dep 實例對象都會被收集到該觀察者實例對象的 this.deps 數組中,因此解除屬性與觀察者之間關係的第二步就是將當前觀察者實例對象從全部的 Dep 實例對象中移除,實現方法就如上代碼所示。
最後會將當前觀察者實例對象的 active 屬性設置爲 false,表明該觀察者對象已經處於非激活狀態了:
this.active = false
以上就是 $watch 方法的實現,以及如何解除觀察的實現。不過不要忘了咱們前面所講的這些內容是假設傳遞給 $watch 方法的第二個參數是一個函數,若是不是函數呢?好比是一個純對象,這時以下高亮的代碼就會被執行:
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this > if (isPlainObject(cb)) { > return createWatcher(vm, expOrFn, cb, options) > } // 省略... }
當參數 cb 不是函數,而是一個純對象,則會調用 createWatcher 函數,並將參數透傳,注意還多傳遞給 createWatcher 函數一個參數,即組件實例對象 vm,那麼 createWatcher 函數作了什麼呢?createWatcher 函數也定義在 src/core/instance/state.js 文件中,以下是 createWatcher 函數的代碼:
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } return vm.$watch(expOrFn, handler, options) }
其實 createWatcher 函數的做用就是將純對象形式的參數規範化一下,而後再經過 $watch 方法建立觀察者。能夠看到 createWatcher 函數的最後一句代碼就是經過調用 $watch 函數並將其返回。來看 createWatcher 函數的第一段代碼:
if (isPlainObject(handler)) { options = handler handler = handler.handler }
由於 createWatcher 函數除了在 $watch 方法中使用以外,還會用於 watch 選項,而這時就須要對 handler 進行檢測。總之若是 handler 是一個純對象,那麼就將變量 handler 的值賦給 options 變量,而後用 handler.handler 的值重寫 handler 變量的值。舉個例子,以下代碼所示:
vm.$watch('name', { handler () { console.log('change') }, immediate: true })
若是你像如上代碼那樣使用 $watch 方法,那麼對於 createWatcher 函數來說,其 handler 參數爲:
handler = { handler () { console.log('change') }, immediate: true }
因此以下這段代碼:
if (isPlainObject(handler)) { options = handler handler = handler.handler }
等價於:
if (isPlainObject(handler)) { options = { handler () { console.log('change') }, immediate: true } handler = handler () { console.log('change') } }
這樣就可正常經過 $watch 方法建立觀察者了。另外咱們注意 createWatcher 函數中以下這段高亮代碼:
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { if (isPlainObject(handler)) { options = handler handler = handler.handler } > if (typeof handler === 'string') { > handler = vm[handler] > } return vm.$watch(expOrFn, handler, options) }
這段代碼說明 handler 除了能夠是一個純對象還能夠是一個字符串,當 handler 是一個字符串時,會讀取組件實例對象的 handler 屬性的值並用該值重寫 handler 的值。而後再經過調用 $watch 方法建立觀察者,這段代碼實現的目的是什麼呢?看以下例子就明白了:
watch: { name: 'handleNameChange' }, methods: { handleNameChange () { console.log('name change') } }
上面的代碼中咱們在 watch 選項中觀察了 name 屬性,可是咱們沒有指定回調函數,而是指定了一個字符串 handleNameChange,這等價於指定了 methods 選項中同名函數做爲回調函數。這就是如上 createWatcher 函數中那段高亮代碼的目的。
上例中咱們使用了 watch 選項,接下來咱們就順便來看一下 watch 選項是如何初始化的,找到 initState 函數,以下:
export function initState (vm: Component) { 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) } }
調用 initWatch 函數,這個函數用來初始化 watch 選項,至於判斷條件咱們就很少講了,前面的講解中咱們已經講解過相似的判斷條件。至於 initWatch 函數,它就定義在 createWatcher 函數的上方
if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }
來看一下 initWatch 的實現,它的定義在 src/core/instance/state.js 中:
function initWatch (vm: Component, watch: Object) { for (const key in watch) { const handler = watch[key] if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) } } }
能夠看到 initWatch 函數就是經過對 watch 選項遍歷,而後經過 createWatcher 函數建立觀察者對象的,須要注意的是上面代碼中有一個判斷條件,以下高亮代碼所示:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) { for (let i = 0; i < handler.length; i++) { createWatcher(vm, key, handler[i]) } } else { createWatcher(vm, key, handler) }}
}
經過這個條件咱們能夠發現 handler 常量能夠是一個數組,handler 常量是什麼呢?它的值是 watch[key],也就是說咱們在使用 watch 選項時能夠經過傳遞數組來實現建立多個觀察者,以下:
watch: {
name: [
function () {
console.log('name 改變了1')
},
function () {
console.log('name 改變了2')
}
]
}
總的來講,在 Watcher 類的基礎上,不管是實現 $watch 方法仍是實現 watch 選項,都變得很是容易,這得益於一個良好的設計。
接下來咱們將會討論深度觀測的實現,在這以前咱們須要回顧一下數據響應的原理,咱們知道響應式數據的關鍵在於數據的屬性是訪問器屬性,這使得咱們可以攔截對該屬性的讀寫操做,從而有機會收集依賴並觸發響應。思考以下代碼:
watch: { a () { console.log('a 改變了') } }
這段代碼使用 watch 選項觀測了數據對象的 a 屬性,咱們知道 watch 方法內部是經過建立 Watcher 實例對象來實現觀測的,在建立 Watcher 實例對象時會讀取 a 的值從而觸發屬性 a 的 get 攔截器函數,最終將依賴收集。但問題是若是屬性 a 的值是一個對象
data () { return { a: { b: 1 } } }, watch: { a () { console.log('a 改變了') } }
如上高亮代碼所示,數據對象 data 的屬性 a 是一個對象,當實例化 Watcher 對象並觀察屬性 a 時,會讀取屬性 a 的值,這樣的確可以觸發屬性 a 的 get 攔截器函數,但因爲沒有讀取 a.b 屬性的值,因此對於 b 來說是沒有收集到任何觀察者的。這就是咱們常說的淺觀察,直接修改屬性 a 的值可以觸發響應,而修改 a.b 的值是觸發不了響應的。
深度觀測就是用來解決這個問題的,深度觀測的原理很簡單,既然屬性 a.b 中沒有收集到觀察者,那麼咱們就主動讀取一下 a.b 的值,這樣不就可以觸發屬性 a.b 的 get 攔截器函數從而收集到觀察者了嗎,其實 Vue 就是這麼作的,只不過你須要將 deep 選項參數設置爲 true,主動告訴 Watcher 實例對象你如今須要的是深度觀測。咱們找到 Watcher 類的 get 方法,以下:
get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw 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 }
如上高亮代碼所示,咱們知道 Watcher 類的 get 方法用來求值,在 get 方法內部經過調用 this.getter 函數對被觀察的屬性求值,並將求得的值賦值給變量 value,同時咱們能夠看到在 finally 語句塊內,若是 this.deep 屬性的值爲真說明是深度觀測,此時會將被觀測屬性的值 value 做爲參數傳遞給 traverse 函數,其中 traverse 函數的做用就是遞歸地讀取被觀察屬性的全部子屬性的值,這樣被觀察屬性的全部子屬性都將會收集到觀察者,從而達到深度觀測的目的。
traverse 函數來自 src/core/observer/traverse.js 文件,以下:
const seenObjects = new Set() /** * Recursively traverse an object to evoke all converted * getters, so that every nested property inside the object * is collected as a "deep" dependency. */ export function traverse (val: any) { _traverse(val, seenObjects) seenObjects.clear() }
上面的代碼中定義了 traverse 函數,這個函數將接收被觀察屬性的值做爲參數,拿到這個參數後在 traverse 函數內部會調用 _traverse 函數完成遞歸遍歷。其中 _traverse 函數就定義在 traverse 函數的下方,以下是 _traverse 函數的簽名:
function _traverse (val: any, seen: SimpleSet) { // 省略... }
_traverse 函數接收兩個參數:
接下來咱們看一下 _traverse 函數是如何遍歷訪問數據對象的,以下是 _traverse 函數的所有代碼:
function _traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } > if (val.__ob__) { > const depId = val.__ob__.dep.id > if (seen.has(depId)) { > return > } > seen.add(depId) > } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
注意上面代碼中高亮的部分,如今咱們把高亮的代碼刪除,那麼 _traverse 函數將變成以下這個樣子:
function _traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
之因此要刪除這段代碼是爲了下降複雜度,如今咱們就當作刪除的那段代碼不存在,來看一下 _traverse 函數的實現,在 _traverse 函數的開頭聲明瞭兩個變量,分別是 i 和 keys,這兩個變量在後面會使用到,接着檢查參數 val 是否是數組,並將檢查結果存儲在常量 isA 中。再往下是一段 if 語句塊:
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return }
遞歸的終止條件
這段代碼是對參數 val 的檢查,後面咱們統一稱 val 爲 被觀察屬性的值,咱們知道既然是深度觀測,因此被觀察屬性的值要麼是一個對象要麼是一個數組,而且該值不能是凍結的,同時也不該該是 VNode 實例(這是Vue單獨作的限制)。只有當被觀察屬性的值知足這些條件時,纔會對其進行深度觀測,只要有一項不知足 _traverse 就會 return 結束執行。因此上面這段 if 語句能夠理解爲是在檢測被觀察屬性的值可否進行深度觀測,一旦可以深度觀測將會繼續執行以後的代碼,以下:
if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) }
這段代碼將檢測被觀察屬性的值是數組仍是對象,不管是數組仍是對象都會經過 while 循環對其進行遍歷,並遞歸調用 _traverse 函數,這段代碼的關鍵在於遞歸調用 _traverse 函數時所傳遞的第一個參數:val[i] 和 val[keys[i]]。這兩個參數其實是在讀取子屬性的值,這將觸發子屬性的 get 攔截器函數,保證子屬性可以收集到觀察者,僅此而已。
如今 _traverse 函數的代碼咱們就解析完了,但你們有沒有想過目前 _traverse 函數存在什麼問題?別忘了前面咱們刪除了一段代碼,以下:
if (val.__ob__) { const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) }
這段代碼的做用不容忽視,它解決了循環引用致使死循環的問題,爲了更好地說明問題咱們舉個例子,以下:
const obj1 = {} const obj2 = {} obj1.data = obj2 obj2.data = obj1
上面代碼中咱們定義了兩個對象,分別是 obj1 和 obj2,而且 obj1.data 屬性引用了 obj2,而 obj2.data 屬性引用了 obj1,這是一個典型的循環引用,假如咱們使用 obj1 或 obj2 這兩個對象中的任意一個對象出如今 Vue 的響應式數據中,若是不作防循環引用的處理,將會致使死循環,以下代碼:
function _traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
若是被觀察屬性的值 val 是一個循環引用的對象,那麼上面的代碼將致使死循環,爲了不這種狀況的發生,咱們可使用一個變量來存儲那些已經被遍歷過的對象,當再次遍歷該對象時程序會發現該對象已經被遍歷過了,這時會跳過遍歷,從而避免死循環,以下代碼所示:
function _traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { const depId = val.__ob__.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i = val.length while (i--) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i--) _traverse(val[keys[i]], seen) } }
如上高亮的代碼所示,這是一個 if 語句塊,用來判斷 val.__ob__ 是否有值,咱們知道若是一個響應式數據是對象或數組,那麼它會包含一個叫作 ob 的屬性,這時咱們讀取 val.__ob__.dep.id 做爲一個惟一的ID值,並將它放到 seenObjects 中:seen.add(depId),這樣即便 val 是一個擁有循環引用的對象,當下一次遇到該對象時,咱們可以發現該對象已經遍歷過了:seen.has(depId),這樣函數直接 return 便可。
以上就是深度觀測的實現以及避免循環引用形成的死循環的解決方案。
一般狀況下當數據狀態發生改變時,全部 Watcher 都爲異步執行,這麼作的目的是出於對性能的考慮。但在某些場景下咱們仍須要同步執行的觀察者,咱們可使用 sync 選項定義同步執行的觀察者,以下:
new Vue({ watch: { someWatch: { handler () {/* ... */}, sync: true } } })
如上代碼所示,咱們在定義一個觀察者時使用 sync 選項,並將其設置爲 true,此時當數據狀態發生變化時該觀察者將以同步的方式執行。這麼作固然沒有問題,由於咱們僅僅定義了一個觀察者而已。
Vue 官方推出了 vue-test-utils 測試工具庫,這個庫的一個特色是,當你使用它去輔助測試 Vue 單文件組件時,數據變動將會以同步的方式觸發組件變動,這對於測試而言會提供很大幫助。你們思考一下 vue-test-utils 庫是如何實現這個功能的?咱們知道開發者在開發組件的時候基本不太可能手動地指定一個觀察者爲同步的,因此 vue-test-utils 庫須要有能力拿到組件的定義並人爲地把組件中定義的全部觀察者都轉換爲同步的,這是一個繁瑣並容易引發 bug 的工做,爲了解決這個問題,Vue 提供了 Vue.config.async 全局配置,它的默認值爲 true,咱們能夠在 src/core/config.js 文件中看到這樣一句代碼,以下:
export default ({ // 省略... /** * Perform updates asynchronously. Intended to be used by Vue Test Utils * This will significantly reduce performance if set to false. */ async: true, // 省略... }: Config)
這個全局配置將決定 Vue 中的觀察者以何種方式執行,默認是異步執行的,當咱們將其修改成 Vue.config.async = false 時,全部觀察者都將會同步執行。其實現方式很簡單,咱們打開 src/core/observer/scheduler.js 文件,找到 queueWatcher 函數:
export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { // 省略... // queue the flush if (!waiting) { waiting = true > if (process.env.NODE_ENV !== 'production' && !config.async) { > flushSchedulerQueue() > return > } nextTick(flushSchedulerQueue) } } }
在非生產環境下若是 !config.async 爲真,則說明開發者配置了 Vue.config.async = false,這意味着全部觀察者須要同步執行,因此只須要把本來經過 nextTick 包裝的 flushSchedulerQueue 函數單獨拿出來執行便可。另外經過如上高亮的代碼咱們也可以明白一件事兒,那就是 Vue.config.async 這個配置項只會在非生產環境生效。
爲了實現同步執行的觀察者,除了把 flushSchedulerQueue 函數從 nextTick 中提取出來以外,還須要作一件事兒,咱們打開 src/core/observer/dep.js 文件,找到 notify 方法,以下:
notify () { // stabilize the subscriber list first const subs = this.subs.slice() > if (process.env.NODE_ENV !== 'production' && !config.async) { > // subs aren't sorted in scheduler if not running async > // we need to sort them now to make sure they fire in correct > // order > subs.sort((a, b) => a.id - b.id) > } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
在異步執行觀察者的時候,當數據狀態方式改變時,會經過如上 notify 函數通知變化,從而執行全部觀察者的 update 方法,在 update 方法內會將全部即將被執行的觀察者都添加到觀察者隊列中,並在 flushSchedulerQueue 函數內對觀察者回調的執行順序進行排序。可是當同步執行的觀察者時,因爲 flushSchedulerQueue 函數是當即執行的,它不會等待全部觀察者入隊以後再去執行,這就沒有辦法保證觀察者回調的正確更新順序,這時就須要如上高亮的代碼,其實現方式是在執行觀察者對象的 update 更新方法以前就對觀察者進行排序,從而保證正確的更新順序。