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