vue計算屬性Computed的小祕密
vue中computed小祕密的發現之旅
首先我們看一段程式碼
<body> <div id="app"> {{ count }} </div> </body> <script> new Vue({ el: '#app', data () { return { num: 66 } }, computed: { count () { console.log(1) return this.num } }, methods: { add () { setInterval(() => { this.num ++ }, 1000) } }, created () { this.add() } }) </script>
請問
-
console.log(1)
會間隔的打印出來嗎? -
html中去掉
{{ count }}
,再問console.log(1)
會間隔的打印出來嗎? - 如果第二問沒有打印出來,那麼在第二問的基礎上怎麼修改才能再次打印出來呢?
我先來揭曉答案
watch
watch: { count: function (oldValue, newValue) { } }
請問為什麼呢?
以下是我的理解,有誤還請指出,共同進步
-
一句話總結就是
computed
是惰性求值,即僅僅定義computed
的話是沒有進行計算屬性count
的依賴收集(可以類似看成data中的數值,僅僅進行了響應式get,set
的定義,並沒有觸發dep.depend
,所以當值發生變化的時候,他並不知道要通知誰,也就不會執行相應的回撥函數了)
原始碼中有這麼一段:
depend () { if (this.dep && Dep.target) {//因為惰性求值,所以Dep.target為false this.dep.depend() } }
所以如果僅僅是computed
的初始化的話並Dep.target
就是undefined
,所以例項化的watch
並不會加入dep的中
看看Computed的實現
- computed初始化
function initComputed (vm: Component, computed: Object) { const watchers = vm._computedWatchers = Object.create(null)//(標記1)新建一個沒有原型鏈的物件,用來存`computed`物件每個值的watch例項物件 const isSSR = isServerRendering()//與服務端渲染有關,暫時忽略 for (const key in computed) { const userDef = computed[key]//取key的值,該值大部分是function型別 //下面主要作用就是在非生產環境中沒有getter,保警告 const getter = typeof userDef === 'function' ? userDef : userDef.get if (process.env.NODE_ENV !== 'production' && getter == null) { warn( `Getter is missing for computed property "${key}".`, vm ) } } if (!isSSR) { //computed中不同的key,也就是計算屬性生成watch例項, //watch作用:簡單看就是當值發生變化時會觸通知到watch,觸發更新,執行回撥函式 watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) } if (!(key in vm)) { //作用是將{key: userDef}變成響應式,重寫其get和set defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { if (key in vm.$data) { warn(`The computed property "${key}" is already defined in data.`, vm) } else if (vm.$options.props && key in vm.$options.props) { warn(`The computed property "${key}" is already defined as a prop.`, vm) } } }
- defineComputed 先看這個函式做了什麼
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } export function defineComputed ( target: any, key: string, userDef: Object | Function ) { const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : userDef sharedPropertyDefinition.set = noop } else { sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : userDef.get : noop sharedPropertyDefinition.set = userDef.set ? userDef.set : noop } Object.defineProperty(target, key, sharedPropertyDefinition) }
上面函式的作用就是改寫get與set
,關鍵就是這個createComputedGetter
在做什麼?
早版本createComputedGetter
的實現是:
function createComputedGetter(){ return function computedGetter () { //這個就是之前用來收集watch例項的一個物件,可看註釋:標記1 const watcher = this._computedWatchers && this._computedWatchers[key] if(watcher) { if(watcher.dirty) { watcher.evaluate() } if(Dep.target){ //這裡也可以看出Dep.target為false時是不會觸發depend,即新增依賴 watcher.depend() } return watcher.value } } }
重點看看watch
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { //進行初始化的定義,忽略無關程式碼 if(options) { this.lazy = !!options.lazy }else { this.lazy = false } this.getter = parsePath(expOrFn) //返回一個取data值得函式 this.dirty = this.lazy//true this.value = this.lazy ? undefined : this.get()//undefined,當不會執行get時也就不會觸發get例項方法中的depend的了 } get () { // 虛擬碼 Dep.target = this //取值也就是訪問觸發屬性的get,get中又觸發dep.depend(),而dep.depend內部觸發的是Dep.target.addDep(this),這裡的this其實是Dep例項 let value = this.getter.call(vm, vm) Dep.target = undefined } addDep (dep: Dep) { //虛擬碼 const id = dep.id if(!this.depIds.has(id)) { this.depIds.add(id) this.deps.push(dep) dep.addSub(this)//this是watch例項物件 } } update () { // 省略... } getAndInvoke (cb: Function) { // 省略... } evaluate () { this.value = this.get() this.dirty = false } depend () { let i = this.deps.length while(i --) { this.deps[i].depend() } } ... }
總結: 1.watcher.dirty
預設為true,執行watcher.evaluate()
所以computed第一次預設會渲染,與watch不同;2.當預設渲染,觸發了get,Dep.target
就不是false,就會執行watcher.depend()
watcher.depend() 早版的實現,它有什麼問題
-
this.dep這個陣列中元素都是Dep的例項物件,watcher所依賴的所有Dep例項化列表;
舉個例子:當計算屬性中return this.num + this.num1
,當讀取計算屬性時會分別觸發num與num1
的get,get中又觸發dep.depend(),而dep.depend內部觸發的是Dep.target.addDep(this),這裡的this其實是Dep例項,這樣就會分別將不同編號的num與num1
的dep,加入到deps中,最後將計算屬性的依賴加入到num,num1
的Dep中, -
這樣就會出現一個問題,當
num
發生改變,觸發set
,觸發其notify
方法即遍歷dep.subDeps陣列(subDeps中放的是各種依賴),觸發依賴的update方法。這樣就導致如果num,num1
發生值得變化,但其和不變,也會造成渲染,這是不合理的
新版本,發生了變化
- 第一個createComputedGetter
function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { watcher.depend() return watcher.evaluate() } } }
- 第二個watcher.depend()
if (this.dep && Dep.target) { this.dep.depend() } }
上面這裡的dep又是哪裡來的呢?在watch類中加了下面程式碼
if (this.computed) { this.value = undefined this.dep = new Dep()//類似一個Object物件,進行observer設定get,set響應式時會進let dep = new Dep, 來收集改值得依賴 } else { this.value = this.get() }
所以從上面的實現,完全可以把一個computed的初始化看出data中資料的初始化,只不過該值又依賴多個依賴
- 第三個evaluate
evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value }
總結
- 計算屬性的觀察者是惰性求值,需要手動通過get
- 怎麼手動get,所以有了問題的第二問,和第三問
-
觸發了get,也就是觸發了
createComputedGetter
函式,就會去取值this.value = this.get()
,進行第一次渲染或取值;同時watcher.depend()
,將計算屬性的依賴
新增至dep中, -
值傳送變化時,輸出
watch.update
,首先判斷是否存在依賴,存在則只需watcher.getAndInvoke(cb)
,
相關程式碼如下:
update () { /* istanbul ignore else */ if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true } else { this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } }, //當計算屬性的值發生變化時,改觸發回撥函式或者進行渲染,而不是通過之前值(例如num改變)變化就觸發回撥 getAndInvoke (cb: Function) { const value = this.get() if ( value !== this.value || isObject(value) || this.deep ) { const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } }