上一篇數據響應式原理對Vue的實現MVVM的核心思想進行了學習,里面提到訂閱-發布模式的訂閱者主要用於響應數據發射變化的更新通知,當然,我們可以這么認為,Vue中的發布者其實也有可能是訂閱者,可以訂閱來自其其它組件的更新通知。本文主要對Vue中有哪些Watcher、在什么時候這些Wathcer會被觸發,以及從源碼角度嘗試總結。
想一下,我們需要數據響應的場景? 比如一個購物車功能,看某寶的購物車界面(自動忽略購買內容 ^^):
在購物車方式下單前,我們需要考慮: 需要選擇哪些來購買,選擇的商品可能買多件,選擇好要購買的商品的時候,我們要對要花費的RMB進行實時計算,也就是點擊頁面的復選框和修改數量的按鈕都會影響核算的總消費,這個就可以利用到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數據做出響應的幾個地方:
1. 通過computed屬性計算選中的購物車條目的總價格;
2. 通過監視選中的條目下發更新功能;
3. 總價格發生變動時,頁面要及時呈現。
1、2、3點基本就蘊含Vue中的幾種Watcher: 1.自定義Watcher; 2. Computed屬性(實際上也是Watcher); 3.渲染Watcher(Render Watcher),接下來對這幾種Watcher細細評味。
1. 自定義Watcher
自定義Watcher可以監視的對象包括基本屬性、對象、數組(后兩種都需要指定deep深層次監聽屬性),具體使用可以看Vue官網watch,好了,知道自定義Wathcer怎么使用,接下來就看一看Vue內部是怎么使用的:
-> 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實例,當然對數據、對象是要循環、遞歸創建。
2. Computed屬性
computed其數據來源是在props或data中定義好的數據(初始化initState時數據能變得可觀察),Vue官網介紹了屬性的用途,主要是解決在template模板中表達式過復雜的問題,都在說computed是基於緩存的,即只有依賴源數據發生改變才會觸發computed對應數據的計算操作,那么,我們應該有好奇它到底是怎么個緩存法,續析computed源碼:
-> 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實現:
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是基於緩存的說法。
3. Render Watcher
組件實例化時會產生一個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)
}
4. 總結
本文簡要分析了Vue中的Watcher類別,並簡要從源碼角度分析了這三種Watcher的實現,文筆粗淺,難免理解不到位,歡迎指正。
另外,歡迎去本人git 可相互學習和star。