前言

在某些情境上,其實使用computed或是watch都可以達成相同需求,可能還可以再加上一個method來一起做個比較,這些做法都可以做到,那要使用哪一個呢?

這個問題令人感到困惑,整理此篇我做研究後的心得。

模板的意義所在

首先,.vue檔案是由以下三個部分所組成的:

1
2
3
4
5
6
7
8
<script setup>
</script>

<template>
</template>

<style>
</style>

最上方是程式碼區塊,中間是模板區塊,而最下面是樣式區塊,彼此各司其職負責自己領域的部分,包括程式邏輯、顯示畫面、畫面樣式,也許寫在一起不是不行,但確實把它們區分開來會讓可讀性增加,也比較好維護。

基於這個原因,template的區塊裡面,會建議只會幫忙做呈現畫面上的處理,頂多只有一些簡單的邏輯像是顯示不顯示各種資料。

複雜的邏輯可以寫嗎?

也是可以寫在template,因為 vue 當中有提供文本插值的方式(如下),使用兩個大括號給包起來,裡面就能放各種值,甚至也可以放置一段表達式(Expressions),代表說也可以在模板的地方寫一些複雜的邏輯部分(如下)。

1
2
3
4
5
6
7
8
// 文本插值的使用方式
<template>
<div> {{ msg }} </div>
</template>

// 文本插值內放置表達式
<div> {{ message.split('').reverse().join('') }} </div>

而這也是我認為computed這個語法的用意,也許可以許多寫法通通都寫在模板上,但如果使用computed可以讓你不用在模板上寫太多複雜的運算,通通把它留在程式邏輯區塊寫就好了。

純模板寫法及 computed

舉一個簡單的例子,小明的年紀要是超過 18 歲就代表成年了,要是少於 18 歲,那麼就代表還沒有成年,這個概念要是寫成程式會像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref } from "vue";
const age = ref(10);
</script>

<template>
<input type="number" v-model="age" />

<div>小明目前{{ age }}歲</div>

<div>{{ age >= 18 ? "小明成年了" : "小明還沒成年" }}</div>
</template>

<style></style>

效果示意:

會發現判斷小明有沒有成年這件事情,是可以都寫在模板上的,但也可以使用computed的方式,讓模板變得相對單純許多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref, computed } from "vue";
const age = ref(10);

const ageComputed = computed(() => {
if (age.value >= 18) return "小明成年了";

return "小明還沒成年";
});
</script>

<template>
<input type="number" v-model="age" />

<div>小明目前{{ age }}歲</div>

<div>{{ ageComputed }}</div>
</template>

<style></style>

改成這個寫法跟上面那種純模板寫法效果一樣,但可以發現模板上的複雜邏輯,都轉換到最上方的程式端。

而要注意,computed會回傳值,回傳回來的是一個響應式的物件,所以要拿一個變數裝它,以及在模板取用時就直接用就好,不需要再呼叫。

舉手時間: 請問什麼是響應式?
vic: 響應式就是對於即時更新相依資料這件事情,而 vue 有提供幫忙的方法,要知道程式中或畫面上的資料改動了,可以利用 vue3 推出的refreactive來做到變成響應式的資料。
舉例來說,畫面上做一個按鈕,功能是按一下變數加一,如果是一個普通的變數,那按按紐不會有反應,不過如果是一個有響應式的變數,那畫面上加一,實際上程式中的變數也會跟著加一,然後更新到畫面上,完畢。

官方推薦用這樣的方式來處理有依靠響應狀態的複雜邏輯,畢竟好寫、好看、好玩。

computed 及 method

method的方式就是在模板上的文本插值內表達式裡面寫一個函式當作方法去呼叫,而效果從外觀上跟上面使用computed一模一樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref } from "vue";
const age = ref(10);

function ageMethod() {
if (age.value >= 18) return "小明成年了";

return "小明還沒成年";
}
</script>

<template>
<input type="number" v-model="age" />

<div>小明目前{{ age }}歲</div>

<div>{{ ageMethod() }}</div>
</template>

<style></style>

使用這種方式,就得在模板上去呼叫這個函式,雖然看似一模一樣,但其實差別還是挺大的,最大的差別是「觸發時機」。

簡單來說,computedmethod一個是響應依賴的東西發生改變的時候才會去計算,一個是畫面做更新的時候就會去計算。

用這個例子來說,computed是去追蹤age這個響應式變數,當它發生改變時,才會去觸發computed內的函式,而method沒有這種概念,無論是不是它所追蹤的,只要畫面有所改變,就會觸發method內的函式。

關於要怎麼觀測到這件事情,首先可以在這例子中新增一個input輸入框,去綁定一個其他不相關的變數,接著把剛剛computedmethod裡面的函式印出’change’,藉此來觀看觸發的時機。

(一) 修改小明年紀輸入框

(二) 修改格外的輸入框

agecomputed所追蹤的響應依賴變數,所以只有當我去修改小明年紀輸入框,才會去觸發computed的函式,如果是修改格外的輸入框,computed就不會去計算,相反的method畫面上任何改變都會觸發一次。

computed不會被重置的特性是源自於自身緩存的能力,只要age的響應式值不改變,那computed永遠不會重新計算,不會觸發。

而這也是官方推薦使用它的原因,因為這種特性使得computed的效能往往有更好的表現,用個比喻:

假如在計算一個非常複雜的東西,帶入X的值去跑可能從頭算一個需要 10 分鐘,但使用computed的話,雖然一開始也要 10 分鐘,但如果第二次我輸入進去的值也是X的話,因為上次已經算過了,就直接拿上次算好的值給你,瞬間就計算結束。

但如果使用method的方法,不論每次是不是帶入 X 去跑,由於都不會把結果存下來,我這次算 10 分鐘,下次一模一樣的輸入也要全部重頭開始。

這就是「緩存」的好處。

如果沒有想要「緩存」的特性,有情境是需要每次畫面更動都想觸發的時候才會選擇使用method,不然一般來說使用computed是較為好的選擇。

監聽的 watch

這裡不會深入講watch,會講些基本的,然後會來跟computed做情境比較。

watch可以監看一個響應式變數的值,可以去撰寫當這個值改變時會發生的事情,所以其實也可以拿來應用在小明年紀判斷的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref, watch } from "vue";
const age = ref(10);

const ageWatch = ref("");

watch(age, (item) => {
if (item >= 18) ageWatch.value = "成年了";
else {
ageWatch.value = "還沒成年";
}
});
</script>

<template>
<div>小明年紀輸入框: <br /><input type="number" v-model="age" /></div>

<div>小明目前{{ age }}歲</div>

<div>{{ ageWatch }}</div>
</template>

<style></style>

watch內的第一個參數是放我所要監看的響應式變數age,接著第二個參數是一個callback,裡面的第一個參數就是最新的age值,所以我可以去藉由判斷這個值去寫邏輯,來去達到跟上方computedmethod相同的效果。

但雖說可以達到相同效果,但我自己個人認為使用watch在這個例子中沒有這麼合適。

來說明一下我的看法,首先可以發現使用watch的話,就會硬生生需要多再宣告一個響應式變數,這樣才能在watch監看觸發變化時,顯示在畫面上,因為watch就是拿來處理以下這些情況的(依據官方):

  1. 需要狀態變化時執行「副作用」
  2. 想要更改 DOM
  3. 處理非同步操作時

會發現這些狀況剛好都是computed不擅長的。

因為computed不應該內部有「副作用」,官方文件這個部分這別強調:
computedgetter 只應該用來做計算而沒有其他副作用。

提問: 而什麼是副作用呢?

vic: 副作用又稱為side effect,我的理解是改動到本身區塊之外的其他東西都算,一個函式的side effect就會取決於改動有沒有超出它function scope的範圍。

舉例來說,像是做非同步的請求(打 api),或是更改 DOM 這種的算是,甚至連console.log都是,而watch很好的補足這一點。

甚至可以說watch就是為此而生的,應該要把這部分的情境都交給它,而非這些狀況的情境,交給computed來完成。

而雖然watchcallback可以是同步也可以是非同步,但我會優先選擇把同步交給只能在同步情境下使用的computed

關於什麼是同步非同步可以參考我之前寫的鐵人賽文章:
JS 之路 Day12 - Asynchronous Programming (非同步程式設計) | Vic’s Blog

總結

computed是屬於比較後期才推出的語法,我會把它歸納成一種優化,而不是必須,有了會更好,但是沒有也一樣可以。

但是確實computed的出現對於整體的code style幫助很大,讓vue寫起來更為優美。

computedwatch共同點都是基於響應式狀態而去使用,透過去即時檢查,去執行相對應的觸發函式。

這也是許多人使用上搞混的原因,因為兩者擁有著些許相同之處。

但差異跟使用情境也同樣明顯:

  • computed適合用在同步的場合,像是把模板上的複雜邏輯拿回來進行加工,在程式的區塊計算完後,再回傳值渲染到模板上。

  • watch適合用在非同步的場合,尤其在響應式狀態變化時需要產生「副作用」的時候,像是發 API,或是要更改 DOM 時。

參考資料