一位同事發的 PR 是要調整 Ranking 的 render 邏輯,看完以後覺得可以紀錄一下雙方的思維差異。
原始版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| const MAX_STAR_SLOTS = 5;
const calcStartNumber = (score: number) => { const normalizedScore = Number.isFinite(score) ? score : 0; return Math.max(0, Math.ceil(normalizedScore / 10)); };
export const getFullStarCount = (score: number): number => { const starCount = calcStartNumber(score); return Math.min(MAX_STAR_SLOTS, Math.floor(starCount / 2)); };
export const showHalfStar = (score: number): boolean => { const starCount = calcStartNumber(score); return starCount % 2 === 1 && getFullStarCount(score) < MAX_STAR_SLOTS; };
export const getEmptyStarCount = (score: number): number => { const full = getFullStarCount(score); const half = showHalfStar(score) ? 1 : 0; return Math.max(0, MAX_STAR_SLOTS - full - half); };
|
1 2 3 4 5 6 7 8 9 10 11
| <i-codicon-star-full v-for="num in getFullStarCount(i.score)" :key="`full-${num}`" class="text-lg text-c-orange-500" /> <i-codicon-star-half v-if="showHalfStar(i.score)" class="text-lg text-c-orange-500" /> <i-codicon-star-full v-for="num in getEmptyStarCount(i.score)" :key="`empty-${num}`" class="text-lg text-c-dark-400" />
|
i-codicon-star-full 這幾個是 icon 元件
第二版
當我看到的時候,留了一句話給同事
要顯示幾顆星的計算應該是一樣,剩下的應該是看圖片要怎麼換,那這樣分空、半、滿 不會太重複嗎?
然後同事就說,他去思考一下,過一陣子就說調整一版了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| const MAX_STAR_SLOTS = 5; interface StarRating { full: number; half: boolean; empty: number; } export type StarIconType = 'full' | 'half' | 'empty';
export const getStarRating = (score: number): StarRating => { const normalizedScore = Number.isFinite(score) ? score : 0; const starCount = Math.max(0, Math.ceil(normalizedScore / 10)); const full = Math.min(MAX_STAR_SLOTS, Math.floor(starCount / 2)); const half = starCount % 2 === 1 && full < MAX_STAR_SLOTS; const empty = Math.max(0, MAX_STAR_SLOTS - full - (half ? 1 : 0)); return { full, half, empty }; };
export const getStarIcons = (score: number): StarIconType[] => { const { full, half, empty } = getStarRating(score); return [ ...Array.from({ length: full }, () => 'full' as const), ...(half ? (['half'] as const) : []), ...Array.from({ length: empty }, () => 'empty' as const), ]; };
|
1 2 3 4 5 6 7 8
| <template v-for="(type, idx) in getStarIcons(i.score)" :key="`${type}-${idx}`"> <i-codicon-star-half v-if="type === 'half'" class="text-lg text-c-orange-500" /> <i-codicon-star-full v-else class="text-lg" :class="type === 'full' ? 'text-c-orange-500' : 'text-c-dark-400'" /> </template>
|
這邊先停下來討論一下這兩個版本的差異:
- 迴圈
- 第一版是每個 icon 根據需要的數量去跑迴圈,寫了三個 for
- 第二版是移到上層,只有一個 for 來跑完全部
- 計算每顆星的呈現
我的版本
我的設計概念是這樣,會有兩個元件
- 一個元件是 render 單顆星,由外面決定呈現的樣式 empty/half/full
- 另一個元件是 n 顆星,由外面決定 n (可以預設5),然後給傳 score 當參數進去
迴圈 i = 1…n
如果 i < score 則 full
如果 i > score 但不是整數則 half
其餘 empty
完整的程式如下
StarIcon.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <script setup lang="ts"> interface Props { type: 'full' | 'half' | 'empty'; }
defineProps<Props>(); </script>
<template> <i-codicon-star-half v-if="type === 'half'" class="text-lg text-c-orange-500" /> <i-codicon-star-full v-else class="text-lg" :class="type === 'full' ? 'text-c-orange-500' : 'text-c-dark-400'" /> </template>
|
StarRating.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <script setup lang="ts"> import StarIcon from './StarIcon.vue';
interface Props { score: number; maxStars?: number; }
const props = withDefaults(defineProps<Props>(), { maxStars: 5, });
type StarType = 'full' | 'half' | 'empty';
/** 根據當前位置和分數計算星星類型 */ const getStarType = (index: number): StarType => { // 將 score (0-100) 轉換為星星數 (0-maxStars) // score 100 = maxStars 顆星 const starScore = (props.score / 100) * props.maxStars;
if (index <= starScore) { return 'full'; }
// 檢查是否需要半星:當 index-1 < starScore < index if (index - 1 < starScore && starScore < index) { return 'half'; }
return 'empty'; }; </script>
<template> <StarIcon v-for="i in maxStars" :key="i" :type="getStarType(i)" /> </template>
|
思維差異
依照同事的思維,是將 計算 和 render 分開來,而我的思維則是這兩個可以放在一起,除非今天計算的部分有需要抽出去,不然這應該屬於元件的行為
另外一個思維,是我將元件的職責分開來,第一個元件只負責 render 星星,第二個元件只負責計算要怎麼呈現
這邊不確定是不是框架所延伸的思維差異,我之前是寫 angular 轉 react,在 react 的世界中就很多會這樣拆元件,在 angular 就比較不會這樣拆
因為我沒有真正的 vue 實戰,也無法確認這件事情
AI 優化版本
請 AI 分析原始版本和我的版本差異,有提到計算是當下呼叫方法,當量大的時候會容易有效能的問題,因此請 AI 幫忙改一個版本
Copilot 搭配 Claude Sonnet 4.5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <script setup lang="ts"> import { computed } from 'vue'; import StarIcon from './StarIcon.vue';
interface Props { score: number; maxStars?: number; }
const props = withDefaults(defineProps<Props>(), { maxStars: 5, });
type StarType = 'full' | 'half' | 'empty';
/** 計算所有星星的類型(只計算一次,結果會被快取) */ const starTypes = computed((): StarType[] => { // 將 score (0-100) 轉換為星星數 (0-maxStars) const starScore = (props.score / 100) * props.maxStars;
return Array.from({ length: props.maxStars }, (_, index) => { const position = index + 1; // 星星位置從 1 開始
if (position <= starScore) { return 'full'; }
// 檢查是否需要半星:當 position-1 < starScore < position if (position - 1 < starScore && starScore < position) { return 'half'; }
return 'empty'; }); }); </script>
<template> <StarIcon v-for="(type, index) in starTypes" :key="index" :type="type" /> </template>
|
這是 AI 說的原理,可以在量大的時候發揮更好的效能
1 2 3 4 5 6 7 8 9
| // 當 score 或 maxStars 改變時: props.score = 85 → starTypes 重新計算 → ['full', 'full', 'full', 'full', 'half'] ↓ 快取這個結果 ↓ // 後續渲染直接使用快取結果,不再重新計算 render 1 → 使用快取 ⚡ render 2 → 使用快取 ⚡ render 3 → 使用快取 ⚡
|