2021-05-12 14:32:11
詳解Vue中的computed和watch
作者:小土豆
部落格園:https://www.cnblogs.com/HouJiao/
掘金:https://juejin.cn/user/2436173500265335
1. 前言
作為一名Vue
開發者,雖然在專案中頻繁使用過computed
和watch
,但從來沒有系統的學習過,總覺得對這兩個知識點有些一知半解
。
如果你也和我一樣,就一起來回顧和總結一下這兩個知識點吧。
本篇非原始碼分析,只是從兩者各自的用法、特性等做一些總結。
2. Vue中的computed
Vue
中的computed
又叫做計算屬性
,Vue官網
中給了下面這樣一個範例。
模板中有一個message
資料需要展示:
<template>
<div id="app">
{{message}}
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
message: 'Hello'
}
}
}
</script>
假如此時有一個需求:對message
進行反轉
並展示到模板中。
那最簡單的實現方式就是直接在模板中做這樣的轉化:
<template>
<div id="app">
<p>{{message}}</p>
<p>{{message.split('').reverse().join('')}}</p>
</div>
</template>
那這個時候,Vue官方
告訴我們:過多的邏輯運算會讓模板變得重且難以維護,而且這種轉化無法複用
,並指導我們使用計算屬性-computed
來實現這個需求。
export default {
name: 'App',
computed: {
reverseMessage: function(){
return this.message.split('').reverse().join('');
}
},
data() {
return {
message: 'Hello'
}
}
}
在以上程式碼中我們定義了一個計算屬性:reverseMessage
,其值為一個函數並返回我們需要的結果。
之後在模板中就可以像使用message
一樣使用reverseMessage
。
<template>
<div id="app">
<p>{{message}}</p>
<p>{{reverseMessage}}</p>
</div>
</template>
那麼此時有人肯定要說了,我用methods
也能實現呀。確實使用methods
也能實現此種需求,但是在這種情況下我們的計算屬性
相較於methods
是有很大優勢的,這個優勢就是計算屬性存在快取
。
如果我們使用methods
實現前面的需求,當message
的反轉
結果有多個地方在使用,對應的methods
函數會被呼叫多次,函數內部的邏輯也需要執行多次;而計算屬性
因為存在快取,只要message
資料未發生變化,則多次存取計算屬性
對應的函數只會執行一次。
<template>
<div id="app">
<p>{{message}}</p>
<p>第一次存取reverseMessage:{{reverseMessage}}</p>
<p>第二次存取reverseMessage:{{reverseMessage}}</p>
<p>第三次存取reverseMessage:{{reverseMessage}}</p>
<p>第四次存取reverseMessage:{{reverseMessage}}</p>
</div>
</template>
<script>
export default {
name: 'App',
computed: {
reverseMessage: function(value){
console.log(" I'm reverseMessage" )
return this.message.split('').reverse().join('');
}
},
data() {
return {
message: 'Hello'
}
}
}
</script>
執行專案,檢視結果,會發現計算屬性reverseMessage
對應的函數只執行了一次。
3. Vue中的watch
Vue
中的watch
又名為偵聽屬性
,它主要用於偵聽資料的變化,在資料發生變化的時候執行一些操作。
<template>
<div id="app">
<p>計數器:{{counter}}</p>
<el-button type="primary" @click="counter++">
Click
</el-button>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
counter: 0
}
},
watch: {
/**
* @name: counter
* @description:
* 監聽Vue data中的counter資料
* 當counter發生變化時會執行對應的偵聽函數
* @param {*} newValue counter的新值
* @param {*} oldValue counter的舊值
* @return {*} None
*/
counter: function(newValue, oldValue){
if(this.counter == 10){
this.counter = 0;
}
}
}
}
</script>
我們定義了一個偵聽屬性counter
,該屬性偵聽的是Vue data
中定義counter
資料,整個的邏輯就是點選按鈕counter
加1
,當counter
等於10
的時候,將counter
置為0
。
上面的程式碼執行後的結果如下:
Vue官網很明確的建議我們這樣使用
watch
偵聽屬性:當需要在資料變化時執行非同步或開銷較大的操作時,這個方式是最有用的
。
4. computed和watch之間的抉擇
看完以上兩部分內容,關於Vue
中computed
和watch
的基本用法算是掌握了。但實際上不止這些,所以接下來我們在來進階學習一波。
這裡我們還原Vue
官網中的一個範例,範例實現的功能大致如下:
該功能可以簡單的描述為:在firstName
和lastName
資料發生變化時,對fullName
進行更新,其中fullName
的值為firstName
和lastName
的拼接。
首先我們使用watch
來實現該功能:watch偵聽firstName和lastName,當這兩個資料發生變化時更新fullName的值
。
<template>
<div id="app">
<p>firstName: <el-input v-model="firstName" placeholder="請輸入firstName"></el-input></p>
<p>lastName: <el-input v-model="lastName" placeholder="請輸入lastName"></el-input></p>
<p>fullName: {{fullName}}</p>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
firstName: '',
lastName: '',
fullName: '(空)'
}
},
// 使用watch實現
watch: {
firstName: function(newValue) {
this.fullName = newValue + ' ' + this.lastName;
},
lastName: function(newValue){
this.fullName = this.firstName + ' ' + newValue;
}
}
}
</script>
接著我們在使用computed
來實現:定義計算屬性fullName,將firstName和lastName的值進行拼接並返回
。
<template>
<div id="app">
<p>firstName: <el-input v-model="firstName" placeholder="請輸入firstName"></el-input></p>
<p>lastName: <el-input v-model="lastName" placeholder="請輸入lastName"></el-input></p>
<p>fullName: {{fullName}}</p>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
firstName: '',
lastName: ''
}
}
computed: {
fullName: function() {
return this.firstName + ' ' + this.lastName;
}
}
}
</script>
我們發現computed
和watch
都可以實現這個功能,但是我們在對比一下這兩種不同的實現方式
:
// 使用computed實現
computed: {
fullName: function() {
return this.firstName + ' ' + this.lastName;
}
},
// 使用watch實現
watch: {
firstName: function(newValue) {
this.fullName = newValue + ' ' + this.lastName;
},
lastName: function(newValue){
this.fullName = this.firstName + ' ' + newValue;
}
}
對比之下很明顯的會發現發現computed
的實現方式更簡潔高階
。
所以在日常專案開發中,對於computed
和watch
的使用要慎重選擇:
這兩者選擇和使用沒有對錯之分,只是希望能更好的使用,而不是濫用。
5. 計算屬性進階
接下來我們在對計算屬性
的內容進行進階學習。
5.1 計算屬性不能和 Vue Data屬性同名
在宣告計算屬性的時候,計算屬性是不能和Vue Data
中定義的屬性同名,否則會出現錯誤:The computed property "xxxxx" is already defined in data
。
如果有閱讀過Vue
原始碼的同學對這個原因應該會比較清楚,Vue
在初始化的時候會按照:initProps-> initMethods -> initData -> initComputed -> initWatch
這樣的順序對資料進行初始化,並且會通過Object.definedProperty
將資料定義到vm
範例上,在這個過程中同名的屬性會被後面的同名屬性覆蓋。
通過列印元件範例物件,可以很清楚的看到
props
、methods
、data
、computed
會被定義到vm
範例上。
5.2 計算屬性的set函數
在前面程式碼範例中,我們的computed
是這麼實現的:
computed: {
reverseMessage: function(){
return this.message.split('').reverse().join('');
}
},
這種寫法實際上是給reverseMessage
提供了一個get
方法,所以上面的寫法等同於:
computed: {
reverseMessage: {
// 計算屬性的get方法
get: function(){
return this.message.split('').reverse().join('');
}
}
},
除此之外,我們也可以給計算屬性
提供一個set
方法:
computed: {
reverseMessage: {
// 計算屬性的get方法
get: function(){
return this.message.split('').reverse().join('');
},
set: function(newValue){
// set方法的邏輯
}
}
},
只有我們主動修改了計算屬性
的值,set
方法才會被觸發。
關於計算屬性
的set
方法在實際的專案開發中暫時還沒有遇到,不過經過一番思考,做出來下面這樣一個範例:
這個範例是分鐘
和小時
之間的一個轉化,利用計算屬性的set
方法就能很好實現:
<template>
<div id="app">
<p>分鐘<el-input v-model="minute" placeholder="請輸入內容"></el-input></p>
<p>小時<el-input v-model="hours" placeholder="請輸入內容"></el-input></p>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
minute: 60,
}
},
computed: {
hours:{
get: function() {
return this.minute / 60;
},
set: function(newValue) {
this.minute = newValue * 60;
}
}
}
}
</script>
5.3 計算屬性的快取
前面我們總結過計算屬性
存在快取,並演示了相關的範例。那計算屬性
的快取
是如何實現的呢?
關於計算屬性
的快取
這個知識點需要我們去閱讀Vue
的原始碼實現,所以我們一起來看看原始碼吧。
相信大家看到
原始碼
這個詞就會有點膽戰心驚
,不過不用過分擔心,文章寫到這裡的時候考慮到本篇文章的內容和側重點,所以不會詳細去解讀計算屬性的原始碼,著重學習計算屬性
的快取
實現,並且點到為止。那如果你沒有仔細解讀過
Vue的響應式原理
,那建議忽略這一節的內容,等對原始碼中的響應式有一定了解之後在來看這一節的內容會更容易理解。( 我自己之前也寫過的一篇相關文章,希望可以給大家參考:1W字長文+多圖,帶你瞭解vue2.x的雙向資料繫結原始碼實現 )
關於計算屬性
的入口原始碼如下:
/*
* Vue版本: v2.6.12
* 程式碼位置:/vue/src/core/instance/state.js
*/
export function initState (vm: Component) {
// ......省略......
const opts = vm.$options
// ......省略......
if (opts.computed) initComputed(vm, opts.computed)
// ......省略 ......
}
接著我們來看看initComputed
:
/*
* Vue版本: v2.6.12
* 程式碼位置:/vue/src/core/instance/state.js
* @params: vm vue範例物件
* @params: computed 所有的計算屬性
*/
function initComputed (vm: Component, computed: Object) {
/*
* Object.create(null):建立一個空物件
* 定義的const watchers是用於儲存所有計算屬性的Watcher範例
*/
const watchers = vm._computedWatchers = Object.create(null)
// 遍歷計算屬性
for (const key in computed) {
const userDef = computed[key]
/*
* 獲取計算屬性的get方法
* 計算屬性可以是function,預設提供的是get方法
* 也可以是物件,分別宣告get、set方法
*/
const getter = typeof userDef === 'function' ? userDef : userDef.get
/*
* 給計算屬性建立watcher
* @params: vm vue範例物件
* @params: getter 計算屬性的get方法
* @params: noop
noop是定義在 /vue/src/shared/util.js中的一個函數
export function noop (a?: any, b?: any, c?: any) {}
* @params: computedWatcherOptions
* computedWatcherOptions是一個物件,定義在本檔案的167行
* const computedWatcherOptions = { lazy: true }
*/
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
// 函數呼叫
defineComputed(vm, key, userDef)
}
}
在initComputed
這個函數中,主要是遍歷計算屬性
,然後在遍歷的過程中做了下面兩件事:
- 第一件:為計算屬性建立
watcher
,即new Watcher
- 第二件:呼叫
defineComputed
方法
那首先我們先來看看new Watcher
都做了什麼。
為了方便大家看清楚
new Watcher
的作用,我將Watcher
的原始碼進行了簡化,保留了一些比較重要的程式碼。同時程式碼中重要的部分都新增了註釋,有些註釋描述的可能有點
重複
或者囉嗦
,但主要是想以這種重複
的方式讓大家可以反覆琢磨並理解原始碼中的內容,方便後續的理解 ~
/*
* Vue版本: v2.6.12
* 程式碼位置: /vue/src/core/observer/watcher.js
* 為了看清楚Watcher的作用
* 將原始碼進行簡化,所以下面是一個簡化版的Watcher類
* 同時部分程式碼順序有所調整
*/
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
) {
// vm為元件範例
this.vm = vm
// expOrFn在new Watcher時傳遞的引數為計算屬性的get方法
// 將計算屬性的get方法賦值給watcher的getter屬性
this.getter = expOrFn
// cb為noop:export function noop (a?: any, b?: any, c?: any) {}
this.cb = cb
// option在new Watcher傳遞的引數值為{lazy: true}
// !!操作符即將options.lazy強轉為boolean型別
// 賦值之後this.lazy的值為true
this.lazy = !!options.lazy
// 賦值之後this.dirty的值true
this.dirty = this.lazy
/*
* 在new Watcher的時候因為this.lazy的值為true
* 所以this.value的值還是undefined
*/
this.value = this.lazy ? undefined : this.get()
}
get () {
const vm = this.vm
/*
* 在建構函式中,計算屬性的get方法賦值給了watcher的getter屬性
* 所以該行程式碼即呼叫計算屬性的get方法,獲取計算屬性的值
*/
value = this.getter.call(vm, vm)
return value
}
evaluate () {
/*
* 呼叫watcher的get方法
* watcher的get方法邏輯為:呼叫計算屬性的get方法獲取計算屬性的值並返回
* 所以evaluate函數也就是獲取計算屬性的值,並賦值給watcher.value
* 並且將watcher.dirty置為false,這個dirty是實現快取的關鍵
*/
this.value = this.get()
this.dirty = false
}
}
看了這個簡化版的Watcher
以後,想必我們已經很清楚的知道了Watcher
類的實現。
那接下來就是關於快取
的重點了,也就是遍歷計算屬性
做的第二件事:呼叫defineComputed
函數:
/*
* Vue版本: v2.6.12
* 程式碼位置:/vue/src/core/instance/state.js
* @params: target vue範例物件
* @params: key 計算屬性名
* @params: userDef 計算屬性定義的function或者object
*/
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
// ......暫時省略有關sharedPropertyDefinition的程式碼邏輯......
/*
* sharedPropertyDefinition本身是一個物件,定義在本檔案31行:
* const sharedPropertyDefinition = {
* enumerable: true,
* configurable: true,
* get: noop,
* set: noop
* }
* 最後使用Object.defineProperty傳入對應的引數使得計算屬性變得可觀測
*/
Object.defineProperty(target, key, sharedPropertyDefinition)
}
defineComputed
方法最核心也只有一行程式碼,也就是使用Object.defineProperty
將計算屬性
變得可觀測。
那麼接下來我們的關注點就是呼叫Object.defineProperty
函數時傳遞的第三個引數:sharedPropertyDefinition
。
sharedPropertyDefinition
是定義在當前檔案中的一個物件
,預設值如下:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
前面貼出來的defineComputed
原始碼中,我註釋說明省略了一段有關sharedPropertyDefinition
的程式碼邏輯,那省略的這段原始碼就不展示了,它的主要作用
就是在對sharedPropertyDefinition.get
和sharedPropertyDefinition.set
進行重寫,重寫之後sharedPropertyDefinition
的值為:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: function(){
// 獲取計算屬性對應的watcher範例
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
},
// set對應的值這裡寫的是noop
// 但是我們要知道set真正的值是我們為計算屬性提供的set函數
// 千萬不要理解錯了哦
set: noop,
}
那sharedPropertyDefinition.get
函數的邏輯已經非常的清晰了,同時它的邏輯就是計算屬性快取
實現的關鍵邏輯:在sharedPropertyDefinition.get
函數中,先獲取到計算屬性
對應的watcher
範例;然後判斷watcher.dirty
的值,如果該值為false
,則直接返回watcher.value
;否則呼叫watcher.evaluate()
重新獲取計算屬性
的值。
關於
計算屬性快取
的原始碼分析就到這裡,相信大家對計算屬性
的快取
實現已經有了一定的認識。不過僅僅是瞭解這些還不夠,我們應該去通讀計算屬性
的完整原始碼實現,才能對計算屬性有一個更通透的認識。
6. 偵聽屬性進階
6.1 handler
前面我們是這樣實現偵聽屬性
的:
watch: {
counter: function(newValue, oldValue){
if(this.counter == 10){
this.counter = 0;
}
}
}
那上面的這種寫法等同於給counter
提供一個handler
函數:
watch: {
counter: {
handler: function(newValue, oldValue){
if(this.counter == 10){
this.counter = 0;
}
}
}
}
6.2 immediate
正常情況下,偵聽屬性
提供的函數是不會立即執行的,只有在對應的vue data
發生變化時,偵聽屬性
對應的函數才會執行。
那如果我們需要偵聽屬性
對應的函數立即執行一次,就可以給偵聽屬性
提供一個immediate
選項,並設定其值為true
。
watch: {
counter: {
handler: function(newValue, oldValue){
if(this.counter == 10){
this.counter = 0;
}
},
immediate: true
}
}
6.3 deep
如果我們對一個物件型別
的vue data
進行偵聽,當這個物件內的屬性發生變化時,預設是不會觸發偵聽函數的。
<template>
<div id="app">
<p><el-input v-model="person.name" placeholder="請輸入姓名"></el-input></p>
<p><el-input v-model="person.age" placeholder="請輸入年齡"></el-input></p>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
person: {
name: 'jack',
age: 20
}
}
},
watch: {
person: function(newValue){
console.log(newValue.name + ' ' + newValue.age);
}
}
}
</script>
監聽物件型別的資料,偵聽函數沒有觸發:
通過給偵聽屬性
提供deep: true
就可以偵聽到物件內部屬性的變化:
watch: {
person: {
handler: function(newValue){
console.log(newValue.name + ' ' + newValue.age);
},
deep: true
}
}
不過仔細觀察上面的範例會發現這種方式去監聽Object
型別的資料,Object
資料內部任一屬性發生變化都會觸發偵聽函數,那如果我們想單獨偵聽物件中的某個屬性,可以使用下面這樣的方式:
watch: {
'person.name': function(newValue, oldValue){
// 邏輯
}
}
7.總結
到此本篇文章就結束了,內容非常的簡單易懂,在此將以上的內容做以總結:
學無止境,除了基礎的內容之外,很多特性的實現原理也是我們應該關注的東西,但是介於本篇文章輸出的初衷,所以對原理實現並沒有完整的分析,後面有機會在總結~
8. 近期文章
9. 寫在最後
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者
文章公眾號
首發,關注 不知名寶藏程式媛
第一時間獲取最新的文章
筆芯❤️~
相關文章