Vue中computed和watch的區(qū)別
前言??
在vue項(xiàng)目中我們常常需要用到computed和watch,那么我們究竟在什么場景下使用computed和watch呢?他們之間又有什么區(qū)別呢?記錄一下!
computed和watch有什么區(qū)別?
相同點(diǎn):(過目一下,下面還會(huì)更新)
- 本質(zhì)上都是一個(gè)watcher實(shí)例,它們都通過響應(yīng)式系統(tǒng)與數(shù)據(jù),頁面建立通信
- 它們都是以Vue的依賴追蹤機(jī)制為基礎(chǔ)的
computed
簡而言之,它的作用就是自動(dòng)計(jì)算我們定義在函數(shù)內(nèi)的“公式”
data() {
return {
num1: 1,
num2: 2
};
},
computed: {
total() {
return this.num1 * this.num2;
}
}
在這個(gè)場景下,當(dāng)this.num1或者this.num2變化時(shí),這個(gè)total的值也會(huì)隨之變化,為什么呢?
## 計(jì)算屬性實(shí)現(xiàn):
由computed是一個(gè)函數(shù)可以看出,它應(yīng)該也有一個(gè)初始化函數(shù) initComputed來對它進(jìn)行初始化。
- 從vue源碼可以看出在initState函數(shù)中對computed進(jìn)行初始化,往下看

- 在initComputed函數(shù)中,有兩個(gè)參數(shù),vm為vue實(shí)例,computed就是我們所定義的computed

具體實(shí)現(xiàn)邏輯就不具體解析了,從上面源碼中可以發(fā)現(xiàn),initComputed函數(shù)會(huì)遍歷我們定義的computed對象,然后給每一個(gè)值綁定一個(gè)watcher實(shí)例

Watcher實(shí)例是響應(yīng)式系統(tǒng)中負(fù)責(zé)監(jiān)聽數(shù)據(jù)變化的角色
計(jì)算屬性執(zhí)行的時(shí)候就會(huì)被訪問到,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),往后如果子級更新了,就會(huì)通知數(shù)組內(nèi)的watcher實(shí)例更新
再看回源碼
const computedWatcherOptions = { lazy: true }
// vm: 組件實(shí)例 computed 組件內(nèi)的 計(jì)算屬性對象
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)記一下)說到這,就能基本了解了計(jì)算watcher實(shí)例在計(jì)算屬性執(zhí)行流程的作用了,即初始化的過程,那么計(jì)算屬性是怎么執(zhí)行的?
從上面的源碼可以看出最下面還有一個(gè)defineComputed函數(shù),它到底是干嘛的?其實(shí)它是vue中用來判斷computed中的key是否已經(jīng)在實(shí)例中定義過,如果未定義,則執(zhí)行defineComputed函數(shù)
來看一下defineComputed函數(shù)

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

