Vue中computed和watch的區(qū)別
前言??
在vue項目中我們常常需要用到computed和watch,那么我們究竟在什么場景下使用computed和watch呢?他們之間又有什么區(qū)別呢?記錄一下!
computed和watch有什么區(qū)別?
相同點:(過目一下,下面還會更新)
- 本質(zhì)上都是一個watcher實例,它們都通過響應(yīng)式系統(tǒng)與數(shù)據(jù),頁面建立通信
- 它們都是以Vue的依賴追蹤機(jī)制為基礎(chǔ)的
computed
簡而言之,它的作用就是自動計算我們定義在函數(shù)內(nèi)的“公式”
data() { return { num1: 1, num2: 2 }; }, computed: { total() { return this.num1 * this.num2; } }
在這個場景下,當(dāng)this.num1或者this.num2變化時,這個total的值也會隨之變化,為什么呢?
## 計算屬性實現(xiàn):
由computed是一個函數(shù)可以看出,它應(yīng)該也有一個初始化函數(shù) initComputed來對它進(jìn)行初始化。
- 從vue源碼可以看出在initState函數(shù)中對computed進(jìn)行初始化,往下看
- 在initComputed函數(shù)中,有兩個參數(shù),vm為vue實例,computed就是我們所定義的computed
具體實現(xiàn)邏輯就不具體解析了,從上面源碼中可以發(fā)現(xiàn),initComputed函數(shù)會遍歷我們定義的computed對象,然后給每一個值綁定一個watcher實例
Watcher實例是響應(yīng)式系統(tǒng)中負(fù)責(zé)監(jiān)聽數(shù)據(jù)變化的角色
計算屬性執(zhí)行的時候就會被訪問到,this.num1和this.num2在Data初始化的時候就被定義成響應(yīng)式數(shù)據(jù)了,它們內(nèi)部會有一個Dep實例,Dep實例就會把這個計算屬性watcher放到自己的sub數(shù)組內(nèi),往后如果子級更新了,就會通知數(shù)組內(nèi)的watcher實例更新
再看回源碼
const computedWatcherOptions = { lazy: true } // vm: 組件實例 computed 組件內(nèi)的 計算屬性對象 function initComputed (vm: Component, computed: Object) { // 遍歷所有的計算屬性 for (const key in computed) { // 用戶定義的 computed const userDef = computed[key] const getter = typeof userDef === 'function' ? userDef : userDef.get watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) defineComputed(vm, key, userDef) }
可以看出在watcher實例在剛被創(chuàng)建時就往
ComputedWatcherOptions
, 傳了{ lazy: true }
, 即意味著它不會立即執(zhí)行我們定義的計算屬性函數(shù),這也意味著它是一個懶計算的功能(標(biāo)記一下)說到這,就能基本了解了計算watcher實例在計算屬性執(zhí)行流程的作用了,即初始化的過程,那么計算屬性是怎么執(zhí)行的?
從上面的源碼可以看出最下面還有一個defineComputed函數(shù),它到底是干嘛的?其實它是vue中用來判斷computed中的key是否已經(jīng)在實例中定義過,如果未定義,則執(zhí)行defineComputed函數(shù)
來看一下defineComputed函數(shù)
- 可以看出這里截取了兩個函數(shù),defineComputed和createComputedGetter兩個函數(shù)
首先說說defineComputed函數(shù)
- 它會判斷是否為服務(wù)器渲染,如果為服務(wù)器渲染則將計算屬性的get、set定義為用戶定義get、set;怎么理解?如果非服務(wù)器渲染的話則在定義get屬性的時候并沒有直接賦值用戶函數(shù),而是返回一個新的函數(shù)computedGetter
- 這里會判斷userDef也就是用戶定義計算屬性key對應(yīng)的value值是否為函數(shù),如果為函數(shù)的話,則將get定義為用戶函數(shù),set賦值為一個空函數(shù)noop;如果不為函數(shù)(對象)則分別取get、set字段賦值
- 在非服務(wù)端渲染中計算屬性的get屬性為computedGetter函數(shù),在每次計算屬性觸發(fā)get屬性時,都會從實例的_computedWatchers(在initComputed已初始化)計算屬性的watcher對象中獲取get函數(shù)(用戶定義函數(shù))
- 至此,計算屬性的初始化就結(jié)束了,最終會把當(dāng)前key定義到vue實例上,也就是可以this.computedKey可以獲取到的原因
細(xì)心的同學(xué)可能發(fā)現(xiàn)了,在上述源碼中還有一行代碼 :Object.defineProperty(target, key, sharedPropertyDefinition),它就是我接下來要說的defineComputed函數(shù)做的第二件事(第一件事就是上面的操作)。當(dāng)訪問一次計算屬性的key 就會觸發(fā)一次 sharedPropertyDefinition(我們自定義的函數(shù)),對computed做了一次劫持,Target可以理解為this,從上面源碼可以看出,每次使用計算屬性,都會執(zhí)行一次computedGetter,跟我們一開始的DEMO一樣,它就會執(zhí)行我們定義的函數(shù),具體怎么實現(xiàn)?
function computedGetter () { // 拿到 上述 創(chuàng)建的 watcher 實例 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 首次執(zhí)行的時候 dirty 基于 lazy 所以是true if (watcher.dirty) { // 這個方法會執(zhí)行一次計算 // dirty 設(shè)置為 false // 這個函數(shù)執(zhí)行完畢后, 當(dāng)前 計算watcher就會推出 watcher.evaluate() } // 如果當(dāng)前激活的渲染watcher存在 if (Dep.target) { /** * evaluate后求值的同時, 如果當(dāng)前 渲染watcher 存在, * 則通知當(dāng)前的收集了 計算watcher 的 dep 收集當(dāng)前的 渲染watcher * * 為什么要這么做? * 假設(shè)這個計算屬性是在模板中被使用的, 并且渲染watcher沒有被對應(yīng)的dep收集 * 那派發(fā)更新的時候, 計算屬性依賴的值發(fā)生改變, 而當(dāng)前渲染watcher不被更新 * 就會出現(xiàn), 頁面中的計算屬性值沒有發(fā)生改變的情況. * * 本質(zhì)上計算屬性所依賴的dep, 也可以看做這個屬性值本身的dep實例. */ watcher.depend() } return watcher.value } }
綜上所述,更加證實了文章開頭所說的計算屬性帶有“懶計算”的功能,為什么呢?回看上面的代碼中的watcher.dirty,在**
計算watcher
實例化的時候,一開始watcher.dirty會被設(shè)置為true**,這樣一說,上面所說的邏輯好像能走通了。走到這里會執(zhí)行watcher的evaluate(),即求值,this.get()簡單理解為執(zhí)行我們定義的計算屬性函數(shù)就可以了。
evaluate () { this.value = this.get() this.dirty = false }
this.dirty
這時候就被變成
false既然這樣,我們是不是可以理解為當(dāng)this.dirty為false時就不會執(zhí)行這個函數(shù)。Vue為什么這樣做? 當(dāng)然是覺得, 它依賴的值沒有變化, 就沒有計算的必要啦
那么問題來了,說了這么久,我們只看到了將this.dirty設(shè)為false,什么時候設(shè)為true呢?來看一下響應(yīng)式系統(tǒng)的set部分代碼
set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val if (newVal === value || (newVal !== newVal && value !== value)) { return } // 通知它的訂閱者更新 dep.notify() }
這段代碼只做兩件事:
1.如果新值和舊值一致,則無需做任何事。
2.如果新值和舊值不一致,則通知這個數(shù)據(jù)下的訂閱者,也就是watcher實例更新
Notity方法就是遍歷一下它的數(shù)組,然后執(zhí)行數(shù)組里每個watcher的update方法
update () { /* istanbul ignore else */ if (this.lazy) { // 假設(shè)當(dāng)前 發(fā)布者 通知 值被重新 set // 則把 dirty 設(shè)置為 true 當(dāng)computed 被使用的時候 就可以重新調(diào)用計算 // 渲染wacher 執(zhí)行完畢 堆出后, 會輪到當(dāng)前的渲染watcher執(zhí)行update // 此時就會去執(zhí)行queueWatcher(this), 再重新執(zhí)行 組件渲染時候 // 會用到計算屬性, 在這時因為 dirty 為 true 所以能重新求值 // dirty就像一個閥門, 用于判斷是否應(yīng)該重新計算 this.dirty = true } }
- 就在這里,
**dirty**
被重新設(shè)置為了**true
**. - 總結(jié)一下dirty的流程:
一開始dirty為true,一旦執(zhí)行了一次計算,就會設(shè)置為false,然后當(dāng)它定義的函數(shù)內(nèi)部依賴的值發(fā)生了變化,則這個值就會重新變?yōu)?strong>true。怎么理解?就拿上面的this.num1和this.num2來說,當(dāng)二者其中一個變化了,dirty的值就變?yōu)?strong>true。
- 說了這么久dirty,那它到底有什么作用?簡而言之,它就是用來記錄我們依賴的值有沒有變,如果變了就重新計算一下值,如果沒變,那就返回以前的值。就像一個懶加載的理念,這也是計算屬性緩存的一種方式。有聰明的同學(xué)又會問了,我們好像一直在讓dirty變成true |false,好像實現(xiàn)邏輯完全跟緩存搭不著邊,也完全沒有涉及到計算屬性函數(shù)的執(zhí)行呀?那我們回頭看看computedGetter函數(shù)
function computedGetter () { // 拿到 上述 創(chuàng)建的 watcher 實例 const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 首次執(zhí)行的時候 dirty 基于 lazy 所以是true if (watcher.dirty) { // 這個方法會執(zhí)行一次計算 // dirty 設(shè)置為 false // 這個函數(shù)執(zhí)行完畢后, 當(dāng)前 計算watcher就會推出 watcher.evaluate() } // 如果當(dāng)前激活的渲染watcher存在 if (Dep.target) { /** * evaluate后求值的同時, 如果當(dāng)前 渲染watcher 存在, * 則通知當(dāng)前的收集了 計算watcher 的 dep 收集當(dāng)前的 渲染watcher * * 為什么要這么做? * 假設(shè)這個計算屬性是在模板中被使用的, 并且渲染watcher沒有被對應(yīng)的dep收集 * 那派發(fā)更新的時候, 計算屬性依賴的值發(fā)生改變, 而當(dāng)前渲染watcher不被更新 * 就會出現(xiàn), 頁面中的計算屬性值沒有發(fā)生改變的情況. * * 本質(zhì)上計算屬性所依賴的dep, 也可以看做這個屬性值本身的dep實例. */ watcher.depend() } return watcher.value } }
這里有一段
Dep.target
的判斷邏輯. 這是什么意思呢.Dep.target
是當(dāng)前正在渲染組件
. 它代指的是你定義的組件, 它也是一個**watcher
**, 我們一般稱之為**渲染watcher**
.計算屬性watcher
, 被通知更新的時候, 會改變**dirty
的值. 而渲染watcher
**被通知更新的時候, 它就會更新一次頁面.顯然我們現(xiàn)在的問題是, 計算屬性的**
dirty
重新變?yōu)?/strong>ture
了, 怎么讓頁面知道現(xiàn)在要重新刷新**了呢?通過**
watcher.depend()
** 這個方法會通知當(dāng)前數(shù)據(jù)的**Dep實例
去收集我們的渲染watcher
. 將其收集起來.當(dāng)數(shù)據(jù)發(fā)生變化的時候, 首先通知計算watcher
更改drity
值, 然后通知渲染watcher
更新頁面.渲染watcher更新
頁面的時候, 如果在頁面的HTML結(jié)果中我們用到了total
這個屬性. 就會觸發(fā)它對應(yīng)的computedGetter
方法. 也就是執(zhí)行上面這部分代碼. 這時候drity
為ture
, 就能如期執(zhí)行watcher.evaluate()
**方法了。至此,computed屬性的邏輯已經(jīng)完畢,總結(jié)來說就是:computed屬性的緩存功能,實際上是通過一個dirty字段作為節(jié)流閥實現(xiàn)的,如果需要重新求值,閥門就打開,否則就一直返回原先的值,而無需重新計算。
watch
watch更多充當(dāng)監(jiān)控者的角色
- 先看例子,當(dāng)total發(fā)生變化時,handler函數(shù)就會被執(zhí)行。
data() { return { total:99 } }, watch: { count: { hanlder(){ console.log('total改變了') } } }
- 相同道理,在watch初始化的時候,肯定有一個initWatch函數(shù),來初始化我們的監(jiān)聽屬性,來到源碼
// src/core/instance/state.js function initWatch (vm: Component, watch: Object) { // 遍歷我們定義的wathcer 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) } } }
- 不難看出,當(dāng)這個函數(shù)拿到我們所定義的watch對象的total對象,然后拿到handler值,當(dāng)然handler也可以是一個數(shù)組,然后傳進(jìn)createWatcher函數(shù)中,那么在這個過程中又做了什么呢?接著看
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) }
- 看得出來,它會解析我們傳進(jìn)來的handler對象,最后調(diào)用**$watch**實現(xiàn)監(jiān)聽,當(dāng)然我們也可以直接通過這個方法實現(xiàn)監(jiān)聽。為什么呢?接著看
Vue.prototype.$watch = function ( expOrFn: string | Function, // 這個可以是 key cb: any, // 待執(zhí)行的函數(shù) options?: Object // 一些配置 ): Function { const vm: Component = this // 創(chuàng)建一個 watcher 此時的 expOrFn 是監(jiān)聽對象 const watcher = new Watcher(vm, expOrFn, cb, options) return function unwatchFn () { watcher.teardown() } }
- 從代碼看的出來,watch函數(shù)∗∗是Vue實例原型上的一個方法,那么我們就可以通過∗∗this∗∗的形式去調(diào)用它。而∗∗watch函數(shù)**是Vue實例原型上的一個方法,那么我們就可以通過**this**的形式去調(diào)用它。而**watch函數(shù)∗∗是Vue實例原型上的一個方法,那么我們就可以通過∗∗this∗∗的形式去調(diào)用它。而∗∗watch屬性就實例化了一個watcher對象,然后通過這個watcher實現(xiàn)了監(jiān)聽,這就是為什么watch和computed本質(zhì)上都是一個watcher對象的原因。那既然它跟computed都是watcher實例,那么本質(zhì)上都是通過Vue響應(yīng)式系統(tǒng)實現(xiàn)的監(jiān)聽,那是不容置疑的。好,到這里我們就要想一個問題,total的Dep實例,是什么時候收集這個watcher實例的?回看實例化時的代碼
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object )
vm
是組件實例, 也就是我們常用的this
expOrFn
是在我們的Demo
中就是total
, 也就是被監(jiān)聽的屬性cb
就是我們的handler函數(shù)
if (typeof expOrFn === 'function') { this.getter = expOrFn } else { // 如果是一個字符則轉(zhuǎn)為一個 一個 getter 函數(shù) // 這里這么做是為了通過 this.[watcherKey] 的形式 // 能夠觸發(fā) 被監(jiān)聽屬性的 依賴收集 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } this.value = this.lazy ? undefined : this.get()
這是**
watcher實例化
的時候, 會默認(rèn)執(zhí)行的一串代碼, 回想一下我們在computed實例化的時候傳入的函數(shù), 也是expOrFn
.** 如果是一個函數(shù)會被直接賦予. 如果是一個字符串. 則**parsePath
通過創(chuàng)建為一個函數(shù). 大家不需要關(guān)注這個函數(shù)的行為, 它內(nèi)部就是執(zhí)行一次this.[expOrFn]
. 也就是this.total
**最后, 因為**
lazy
是false
. 這個值只有計算屬性的時候才會被傳true
.所以首次會執(zhí)行this.get()
**.get
里面則是執(zhí)行一次getter()
觸發(fā)響應(yīng)式到這里監(jiān)聽屬性的初始化邏輯就算是完成了, 但是在數(shù)據(jù)更新的時候, 監(jiān)聽屬性的觸發(fā)還有與計算屬性不一樣的地方.
監(jiān)聽屬性是異步觸發(fā)的,為什么呢?因為監(jiān)聽屬性的執(zhí)行邏輯和組件的渲染是一樣的,他們都會放到一個nextTick函數(shù)中,放到下一次Tick中執(zhí)行
總結(jié)
說了這么多關(guān)于這兩座大山的相關(guān)內(nèi)容,也該來總結(jié)一下了。
相同點:
- 本質(zhì)上都是一個watcher實例,它們都通過響應(yīng)式系統(tǒng)與數(shù)據(jù),頁面建立通信,只是行為不同
- 計算屬性和監(jiān)聽屬性對于新值和舊值一樣的賦值操作,都不會做任何變化,不過這一點的實現(xiàn)是在響應(yīng)式系統(tǒng)完成的。
- 它們都是以Vue的依賴追蹤機(jī)制為基礎(chǔ)的
不同點:
- 計算屬性具有“懶計算”功能,只有依賴的值變化了,才允許重新計算,成為"緩存",感覺不夠準(zhǔn)確。
- 在數(shù)據(jù)更新時,計算屬性的dirty狀態(tài)會立即改變,而監(jiān)聽屬性與組件重新渲染,至少會在下一個"Tick"執(zhí)行。
#感謝
至此,本篇有關(guān)computed和watch屬性的相關(guān)內(nèi)容到此就結(jié)束啦,有什么補(bǔ)充的可以聯(lián)系我哦!
以上就是Vue中computed和watch的區(qū)別的詳細(xì)內(nèi)容,更多關(guān)于Vue computed和watch的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue實現(xiàn)元素拖動并互換位置的實現(xiàn)代碼
在使用Vue的場景下,需要實現(xiàn)對元素進(jìn)行拖動交換位置,接下來通過本文給大家介紹vue實現(xiàn)元素拖動并互換位置的實現(xiàn)代碼,需要的朋友可以參考下2023-09-09解決ElementUI中tooltip出現(xiàn)無法顯示的問題
這篇文章主要介紹了解決ElementUI中tooltip出現(xiàn)無法顯示的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03vue中v-for數(shù)據(jù)狀態(tài)值變了,但是視圖沒改變的解決方案
這篇文章主要介紹了vue中v-for數(shù)據(jù)狀態(tài)值變了,但是視圖沒改變的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06vue2如何使用vue-i18n搭建多語言切換環(huán)境
這篇文章主要介紹了vue2-使用vue-i18n搭建多語言切換環(huán)境的相關(guān)知識,在data(){}中獲取的變量存在更新this.$i18n.locale的值時無法自動切換的問題,需要刷新頁面才能切換語言,感興趣的朋友一起看看吧2023-12-12vue?el-table實現(xiàn)動態(tài)添加行和列具體代碼
最近遇到一個動態(tài)增加行和列的需求,所以這里給大家總結(jié)下,這篇文章主要給大家介紹了關(guān)于vue?el-table實現(xiàn)動態(tài)添加行和列的相關(guān)資料,需要的朋友可以參考下2023-09-09