在項目中computed和watch很是實用,它們是如何實現的呢?編程
咱們的編碼目標是下面的demo可以成功渲染,最終渲染結果<h1>未讀消息:2</h1>
。設計模式
let v = new Vue({
el: '#app',
data () {
return {
news: [1]
}
},
computed: {
newsCount() {
return this.news.length
},
},
render (h) {
return h('h1', '未讀消息:' + this.newsCount)
}
})
setTimeout(() => {
v.news.push(2)
}, 1000)
複製代碼
根據上圖能夠知道,Vue將數據構造爲響應式的,若是須要監聽數據則要新建Watch
的實例,創建Dep和Watch之間聯繫。緩存
watch的常見用法以下:bash
watch: {
news () {
console.log('watch news!')
}
}
複製代碼
watch功能依託Watch
類實現,在Vue初始化時,爲全部watch屬性建立Watch
實例。閉包
function initWatch(vm: Vue) {
const watch = vm.$options.watch
for (let key in watch) {
new Watch(vm._proxyThis, key, watch[key], { user: true })
}
}
複製代碼
new Watch
實例化過程當中,會將key
轉爲函數,執行該函數能夠獲取被監聽的屬性值,另外會將watch[key]
函數保存在this.cb
變量中。app
this.getter = isFunction(key) ? key : parsePath(key) || noop
this.cb = cb
function parsePath(key: string): any {
return function(vm: any) {
return vm[key]
}
}
複製代碼
接着直接執行上一步的函數this.getter
,收集全部依賴。異步
private get(): any {
let vm = this.vm
pushTarget(this)
let value = this.getter.call(vm, vm)
popTarget()
return value
}
複製代碼
當被監聽屬性發生變化時,會通知Watch
實例進行更新,從而執行this.cb.call(vm, value, this.value)
函數。函數
computed的調用形式主要有如下兩種:oop
computed: {
newsCount() {
return this.news.length
},
newsStr: {
get () {
return this.news.join(',')
},
set (val) {
this.news = val.split(',')
}
}
}
複製代碼
computed屬性能夠定義get
和set
函數,所以比較特殊:1.它的值依賴於其餘數據屬性;2.修改它也會驅動視圖進行更新。post
computed功能一樣依賴Watch
類實現,在Vue初始化時,爲全部的computed屬性建立watch實例:new Watch(vm._proxyThis, getter, noop, {lazy: true})
。
function initComputed(vm: Vue) {
let proxyComputed: any
const computed = vm.$options.computed
if (!isPlainObject(computed)) return
for (let key in computed) {
let userDef = computed[key]
let getter = isFunction(userDef) ? userDef : userDef.get
vm._computedWatched[key] = new Watch(vm._proxyThis, getter, noop, {
lazy: true
})
}
vm.$options.computed = proxyComputed = observeComputed(
computed,
vm._computedWatched,
vm._proxyThis
)
for (let key in computed) {
proxyForVm(vm._proxyThis, proxyComputed, key)
}
}
複製代碼
接着將computed屬性自己設置爲響應式,同時調用createComputedGetter
對屬性進行封裝。
當修改computed屬性時,computed觸發閉包變量dep.notify
通知渲染更新。
當修改news
屬性時,會觸發Vue進行渲染更新,在從新獲取computed屬性值的時候,會執行createComputedGetter
封裝後的函數,其本質是執行上一步的getter
函數,並將計算結果返回。
function observeComputed(obj: VueComputed, _computedWatched: any, proxyThis: any): Object {
if (!isPlainObject(obj) || isProxy(obj)) return obj
let proxyObj = createProxy(obj)
for (let key in obj) {
defineComputed(proxyObj, key, obj[key], _computedWatched[key], proxyThis)
}
return proxyObj
}
function defineComputed(
obj: any,
key: string,
userDef: VueComputedMethod,
watcher: any,
proxyThis: any
): void {
if (!isProxy(obj)) return
let dep: Dep = new Dep()
const handler: any = {}
if (isFunction(userDef)) {
handler.get = createComputedGetter(watcher)
handler.set = noop
} else if (isObject(userDef)) {
handler.get = createComputedGetter(watcher)
handler.set = userDef.set || noop
}
defineProxyObject(obj, key, {
get(target, key) {
Dep.Target && dep.depend()
return handler.get.call(proxyThis)
},
set(target, key, newVal) {
handler.set.call(proxyThis, newVal)
dep.notify()
return true
}
})
}
function createComputedGetter(watcher: Watch): Function {
return function computedGetter() {
if (watcher) {
// 計算值
watcher.evaluate()
// 將computed-dep添加watch對象
Dep.Target && watcher.depend()
return watcher.value
}
}
}
複製代碼
分析computed確定要提到其緩存特性,這又是如何實現的?
咱們知道獲取computed的屬性值時,會執行createComputedGetter
封裝後的函數,經過給Watch
類添加dirty
屬性控制是否從新計算computed的屬性值。Watch
類的其餘函數中確定須要配合修改,如evaluate
和update
方法。
function createComputedGetter(watcher: Watch): Function {
return function computedGetter() {
if (watcher) {
// 計算值
if (watcher.dirty) {
watcher.evaluate()
}
// 將computed-dep添加watch對象
Dep.Target && watcher.depend()
return watcher.value
}
}
}
複製代碼
Vue在實例化過程當中,會對傳入的數據進行初始化處理。
首先確定是爲prop、data建立閉包變量dep,接着纔是初始化computed和watch的屬性,在後者中建立Watch
實例監聽屬性的變化。
initProps(this)
initMethods(this)
initData(this)
initComputed(this)
initWatch(this)
複製代碼
Vue的響應式渲染依賴Dep和Watch,computed和watch功能也依賴它們,另外,Vue還封裝了方法$watch
對屬性進行監聽。爲了支持上述功能,Watch和Dep添加了一些配置項,在理解源碼時,能夠進行必定忽略。
Dep和Watch設計的至關巧妙,咱們本身編程能不能想到這樣的方式?推薦學習下設計模式,或許能有所幫助。