詳解Vue中的computed和watch
作者:小土豆
部落格園://www.cnblogs.com/HouJiao/
掘金://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. 寫在最後
如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者
文章公眾號
首發,關注 不知名寶藏程式媛
第一時間獲取最新的文章
筆芯❤️~