iT邦幫忙

2024 iThome 鐵人賽

DAY 16
0
Modern Web

欸你是要進 Vue 了沒?系列 第 16

欸你是要進 Vue 了沒? - Day16:Vue 的 computed 在算什麼東西

  • 分享至 

  • xImage
  •  

我們接續響應式基礎的系列~今天帶大家看 computed 這個語法。
/images/emoticon/emoticon78.gif

使用情境

computed?「計算」的意思。

那又是在計算什麼東西,為什麼 Vue 會有特別一個這個語法呢?

官方文件:模板中的表達式雖然方便,但也只能用來做簡單的操作。如果在模板中書寫太多邏輯,會讓模板變得臃腫,難以維護。

因此我們推薦使用計算屬性來描述依賴響應式狀態的複雜邏輯。

在模板中其實可以使用 JS 表達式計算一些邏輯,但綁定表達式必須注意:在每次渲染的時候,都會去呼叫這些表達式,可想而知⋯⋯當資料越來越複雜,就會比較難維護被主管念

可以用以下範例慢慢感受一下差異被念的感覺

範例

鐵人賽開始了!
而 ++ 每次完成發文後,都會把當天「發文的主題」添加一筆到「資料庫」。

例如~假設現在進入到第 3 天:
資料庫是:myIronmanData 這個響應式物件。
發文的主題是:myIronmanData 物件的屬性 topics

<script setup>
const myIronmanData = reactive({
  name: "++",
  topics: [
    "Day1:前言/自我介紹小廢話",
    "Day2:Vue 為啥我要認識你",
    "Day3:Vue 請你自我介紹五分鐘"
  ],
});

然後 ++ 會想在「介面」上知道當天是否已經過了一半的鐵人賽!
可能會這樣做:

<template>
  <h3>今天是 9/15,鐵人賽已經過一半了嗎?</h3>
  <span>{{
    myIronmanData.topics.length >= 15
      ? "對!你一定可以完賽的"
      : "還沒!繼續加油!"
  }}</span>
  <br />
  <h3>今天是 9/16,鐵人賽已經過一半了嗎?</h3>
  <span>{{
    myIronmanData.topics.length >= 15
      ? "對!你一定可以完賽的"
      : "還沒!繼續加油!"
  }}</span>
  <br />
  <h3>今天是 9/17,鐵人賽已經過一半了嗎?</h3>
  <span>{{
    myIronmanData.topics.length >= 15
      ? "對!你一定可以完賽的"
      : "還沒!繼續加油!"
  }}</span>
</template>

此模板中放了 JS 表達式:
會判斷 物件 myIronmanData 中的 topics 陣列長度 是否大於等於 15,並呈現出相應的字串。

<span>{{
    myIronmanData.topics.length >= 15
      ? "對!你一定可以完賽的"
      : "還沒!繼續加油!"
  }}</span>

看起來沒有毛病,很棒很正確。
https://ithelp.ithome.com.tw/upload/images/20240929/201691393CHk31GmbD.png

但是,是否有發現,在這個 template 中,一直重複寫了 myIronmanData.topics.length >= 15 ? "對!你一定可以完賽的" : "還沒!繼續加油!" 嗎?
也就是:每次渲染時都會重新執行相同的邏輯、重新計算一次。

computed:交給我來吧

我們可以用它寫一段邏輯:

const isPassHalfIronman = computed(() => {
  return myIronmanData.topics.length >= 15
    ? "對!你一定可以完賽的"
    : "還沒!繼續加油!";
});

然後在模板上呈現:

<template>
  <h3>今天是 9/15,鐵人賽已經過一半了嗎?</h3>
  <span>{{ isPassHalfIronman }}</span>
  <br />
  <h3>今天是 9/16,鐵人賽已經過一半了嗎?</h3>
  <span>{{ isPassHalfIronman }}</span>
  <br />
  <h3>今天是 9/17,鐵人賽已經過一半了嗎?</h3>
  <span>{{ isPassHalfIronman }}</span>
</template>

computed 和一般函式的差異

其實看起來很像包成一支 function 可以做到的事啊?

我們分別使用 computed 和一般函式來試試看:

// 使用 computed 範例
const isPassHalfIronman = computed(() => {
  return myIronmanData.topics.length >= 15
    ? "對!你一定可以完賽的"
    : "還沒!繼續加油!";
});

// 使用一般函式
function isPassHalfIronmanFunction() {
  return myIronmanData.topics.length >= 15
    ? "對!你一定可以完賽的"
    : "還沒!繼續加油!";
}
</script>

<template>
  <h3>今天是 9/15,鐵人賽已經過一半了嗎?</h3>
  <span>使用 computed:{{ isPassHalfIronman }}</span>
  <br />
  <span>使用 function:{{ isPassHalfIronmanFunction() }}</span>
  <br />
</template>

瀏覽器上的結果:
https://ithelp.ithome.com.tw/upload/images/20240929/20169139EmUpJvgjlK.png

因此,其實一般函式也是可以做到,但差別就在於運作機制:

官方文件:若我們將同樣的函數定義為一個方法而不是計算屬性,兩種方式在結果上確實是完全相同的,然而,不同之處在於計算屬性值會基於其響應式依賴被緩存。一個計算屬性僅會在其響應式依賴更新時才重新計算。

  • 使用普通函式時:會在每次渲染時,都呼叫函式,並重新計算其結果。
  • 使用 computed:因為 computed「依賴於響應式系統的緩存」,只有在響應式的值「改變」的時候,才會更新。
    因此提升了效能!

而官方文件用了一個很好的例子闡述兩者差異:

這也解釋了為什麼下面的計算屬性永遠不會更新,因為 Date.now() 並不是一個響應式依賴:

const now = computed(() => Date.now())

MDN:Date.now() 方法返回自 1970 年 1 月 1 日 00:00:00 (UTC) 到当前时间的毫秒数。

https://ithelp.ithome.com.tw/upload/images/20240929/20169139qokG9OAhA0.png

Date.now()computed 中被執行,因為它「並不是 Vue 的響應式數據系統的一部分」,只是單純 return 了毫秒數,並沒有被 Vue 監測、不會觸發介面更新。

特性

唯讀的,無法被修改、寫入值

我們先來看一下如果 computed 物件本身被印出來會長怎樣,這是剛剛的isPassHalfIronman 物件:

https://ithelp.ithome.com.tw/upload/images/20240929/201691395oRBRi39dz.png

其中有 _value 代表著值,我們試著這樣取值:

console.log(isPassHalfIronman.value);

我們嘗試修改值,並且再印出來瞧瞧:

console.log(isPassHalfIronman.value = "Not yet! keep going babeeeeeee!");
console.log(isPassHalfIronman);

這時候拋出了一個警告,並且沒有更改內部的值:
https://ithelp.ithome.com.tw/upload/images/20240929/201691399164uNtrsy.png

警告也表明了不能在這邊修改 computed 的值。
06

官方文件:只在某些特殊場景中你可能才需要用到“可寫”的屬性,你可以通過同時提供 getter 和 setter 來創建:

我們這樣試試看:

<script setup>
import { reactive, computed } from "vue";

const myIronmanData = reactive({
  name: "++",
  topics: [
    "Day1:前言/自我介紹小廢話",
    "Day2:Vue 為啥我要認識你",
    "Day3:Vue 請你自我介紹五分鐘",
  ],
});

// 使用 computed 範例,可讀可寫
const isPassHalfIronman = computed({
  // getter
  get() {
    return myIronmanData.topics.length >= 15
      ? "對!你一定可以完賽的"
      : "還沒!繼續加油!";
  },
  // setter
  set() {
    myIronmanData.topics = Array(15).fill("一鍵完賽");
    console.log(myIronmanData.topics);
  },
});
</script>

<template>
  <h3>今天是 9/17,鐵人賽已經過一半了嗎?</h3>
  <h3>{{ isPassHalfIronman }}</h3>
  <button @click="isPassHalfIronman = '還沒!繼續加油!'">直接完賽!</button>
</template>

我們可在原本的 isPassHalfIronman 物件中,設置:

  1. get() 方法(取值時會觸發):return 原本的邏輯。
  2. set() 方法(更改值時會觸發):將陣列長度設置為 15,並印出陣列。

並將物件綁定在 @click 事件中,點擊按鈕後會觸發 'isPassHalfIronman = '還沒!繼續加油!',而這是修改值的行為,將會執行 isPassHalfIronman 中的 setter 邏輯,因此將會把 myIronmanData.topics 陣列 fill15 個元素。

這麼一來,按鈕按下後,瀏覽器上我們看到什麼?

computed 物件中被響應性綁定的值,成功被修改,陣列裡變成 15 個元素,介面也更新了!

但是完賽了嗎? 並沒有 XD

最佳實踐

getter 不應該有副作用

這邊說的副作用是:不應該在 computed 物件其中像上述談到的:變更值、狀態,非同步等等。

官方文件:在之後的指引中我們會討論如何使用偵聽器根據其他響應式狀態的變更來創建副作用。

避免直接修改屬性值​

官方文件:從計算屬性返回的值是派生狀態。可以把它看作是一個“臨時快照”,每當源狀態發生變化時,就會創建一個新的快照。更改快照是沒有意義的,因此計算屬性的返回值應該被視為只讀的,並且永遠不應該被更改——應該更新它所依賴的源狀態以觸發新的計算。

GPT:“派生狀態”是指基於其他響應式狀態計算而得出的值。

因為 computed 中的值通常是「依賴於其他響應性綁定」的,而且當綁定的東西更改了值,會自動更新,因此最好是直接去變動響應性根源的狀態,而不是在這邊改它!

小結

讀這篇的時候認真有感欸,好像前面學的基礎概念都漸漸在這邊被串起來了(happy)(大家一起來讀文件學 Vue 吧?)
/images/emoticon/emoticon24.gif

範例 code ⬇️

https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day16

參考資料


上一篇
欸你是要進 Vue 了沒? - Day15:Vue 你怎麼 DOM 起來了?乂稀奇古怪的 ref && reactive 解包合體技乂
下一篇
欸你是要進 Vue 了沒? - Day17:Vue 屬性綁定之 class && style 功能增強系列(class 篇)
系列文
欸你是要進 Vue 了沒?22
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
jeremykuo
iT邦新手 5 級 ‧ 2024-09-30 01:11:14

有了這篇終於不用被主管唸啦!但鐵人賽還沒有完賽。/images/emoticon/emoticon04.gif

++ iT邦新手 5 級 ‧ 2024-09-30 01:14:58 檢舉

有在認真看 讚

我要留言

立即登入留言