上一篇數據響應式原理對Vue的實現MVVM的核心思想進行了學習,裏面提到訂閱-發佈模式的訂閱者主要用於響應數據發射變化的更新通知,固然,咱們能夠這麼認爲,Vue中的發佈者其實也有多是訂閱者,能夠訂閱來自其其它組件的更新通知。本文主要對Vue中有哪些Watcher、在何時這些Wathcer會被觸發,以及從源碼角度嘗試總結。javascript
想一下,咱們須要數據響應的場景? 好比一個購物車功能,看某寶的購物車界面(自動忽略購買內容 ^^):html
在購物車方式下單前,咱們須要考慮: 須要選擇哪些來購買,選擇的商品可能買多件,選擇好要購買的商品的時候,咱們要對要花費的RMB進行實時計算,也就是點擊頁面的複選框和修改數量的按鈕都會影響覈算的總消費,這個就能夠利用到Vue裏面的計算屬性了:vue
new Vue({ name: 'cart', data () { return { selectedCarts: [] } }, watch: { /** * 監視selectedCarts變化 * */ selectedCarts: { handler: function (oldVal, newVal) { // do something }, deep: true } }, computed: { /** * 計算總價格 * @returns {number} */ totalPrice () { let totalPrice = 0.0 this.selectedCarts.forEach((cart) => { totalPrice += cart.num * cart.price; }) return totalPrice; } } });
上面示例,就能夠computed裏的總價格totalPrice就能夠根據選中的購物車條目selectedCarts計算得出,在計算出總價格後,會在頁面呈現出計算的結果。此外,咱們能夠經過Vue的watch屬性觀察selectedCarts的變化,根據新舊值比較,能夠下發更新購物車記錄操做(數量)。咱們來看一下這個例子中須要Vue數據作出響應的幾個地方:java
1. 經過computed屬性計算選中的購物車條目的總價格; 2. 經過監視選中的條目下發更新功能; 3. 總價格發生變更時,頁面要及時呈現。
一、二、3點基本就蘊含Vue中的幾種Watcher: 1.自定義Watcher; 2. Computed屬性(實際上也是Watcher); 3.渲染Watcher(Render Watcher),接下來對這幾種Watcher細細評味。node
自定義Watcher能夠監視的對象包括基本屬性、對象、數組(後兩種都須要指定deep深層次監聽屬性),具體使用能夠看Vue官網watch,好了,知道自定義Wathcer怎麼使用,接下來就看一看Vue內部是怎麼使用的:git
-> vue/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) } } } function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) { // 若是指定的參數爲純對象如: // a: { // hander: 'methodName', // deep: Boolean // } if (isPlainObject(handler)) { options = handler handler = handler.handler } // 若是handler是字符串,則表示方法名,須要根據方法名來獲取到該方法的句柄 if (typeof handler === 'string') { handler = vm[handler] } // 內部調用$watch return vm.$watch(expOrFn, handler, options) } // $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) } options = options || {} // 用戶自定義Watcher options.user = true // 建立一個Watcher實例 const watcher = new Watcher(vm, expOrFn, cb, options) // 當即執行回調? if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() } }
對應watch中的所觀察的數據進行初始化操做,實際上就是爲它們建立一個Watcher實例,固然對數據、對象是要循環、遞歸建立。github
computed其數據來源是在props或data中定義好的數據(初始化initState時數據能變得可觀察),Vue官網介紹了屬性的用途,主要是解決在template模板中表達式過複雜的問題,都在說computed是基於緩存的,即只有依賴源數據發生改變纔會觸發computed對應數據的計算操做,那麼,咱們應該有好奇它究竟是怎麼個緩存法,續析computed源碼:express
-> src/core/instance/state.js function initComputed (vm: Component, computed: Object) { // $flow-disable-line const watchers = vm._computedWatchers = Object.create(null) // computed properties are just getters during SSR const isSSR = isServerRendering() for (const key in computed) { // 獲取對應computed屬性的定義 function或者表達式 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 // 定義了屬性: { lazy: true } ) } ... // component-defined computed properties are already defined on the // component prototype. We only need to define computed properties defined // at instantiation here. ... defineComputed(vm, key, userDef) ... } }
遍歷options中的computed屬性並在非服務器渲染方式的狀況下,依次爲每個計算屬性產生一個Watcher,即computed就是依賴Watcher實現的,但具體和普通的Watcher有什麼不一樣?(後面會進行介紹),繼續看defineComputed實現:api
export function defineComputed ( target: any, key: string, userDef: Object | Function ) { // 非服務器端渲染,則用緩存 const shouldCache = !isServerRendering() if (typeof userDef === 'function') { // 函數方式 sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop // 空函數 } else { // 對象方式 sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop sharedPropertyDefinition.set = userDef.set || noop } ... // 數據劫持 Object.defineProperty(target, key, sharedPropertyDefinition) }
找到efineComputed中的核心方法createComputedGetter,主要是設置數據劫持操做的getter方法:數組
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { // dirty標誌數據是否發生變化 watcher.evaluate() // 執行watcher.get()方法,並設置dirty爲false } if (Dep.target) { // 收集依賴 watcher.depend() } return watcher.value } } }
這兒咱們就基本探索到computed屬性計算的核心操做,咱們經過判斷當前watcher(computed)的dirty標誌位判斷是否須要進行重新計算即執行watcher.evaluate內部的watcher.get方法,並設置dirty屬性爲false(主要是在執行get後重置數據爲未更新狀態,便於後續的觀察操做),咱們用購物車示例中的選中的購物車data.selectedCarts數據源結合數據響應式原理講到的數據訂閱-發佈模式來簡單分析一下這個計算過程,給出一個計算流程圖:
說明:
1. 更新購物車選中條目or更新條目購買數量 2. 觸發選中購物車條目selectedCarts的setter進行數據劫持處理 3. setter通知觀察者notify->update->設置totalPrice對應的Watcher的dirty=true 4. 頁面renderWatcher準備渲染,經過調用totalPriceWatcher的computedGetter的evaluate->get,而後回調totalPrice()方法,計算結果;注意在若是totalPrice依賴的數據源selectedCarts未發生改變時,就會經過computedGetter方法直接返回以前的數據(watcher.value),這也就應證了以前所說的computed是基於緩存的說法。
組件實例化時會產生一個Watcher,在組件$mount的時候,在mountComponent()
中會實例化一個Watcher,並掛載到vm的_watchers上,這個Watcher最終會回調Vue的渲染函數從而完成Vue的更新渲染:
new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) updateComponent = () => { // vm._render() 由vm.$options.render()生成的vnode節點 vm._update(vm._render(), hydrating) }
本文簡要分析了Vue中的Watcher類別,並簡要從源碼角度分析了這三種Watcher的實現,文筆粗淺,不免理解不到位,歡迎指正。另外,歡迎去本人git 相互學習和star,不勝感激。