[Medium] 📄 Vue Basic & API
1. Can you describe the core principles and advantages of the framework Vue?
請描述 Vue 框架的核心原理和優勢
核心原理
Vue 是一個漸進式的 JavaScript 框架,其核心原理包含以下幾個重要概念:
1. 虛擬 DOM(Virtual DOM)
使用虛擬 DOM 來提升效能。它只會更新有變化的 DOM 節點,而不是重新渲染整個 DOM Tree。透過 diff 演算法比較新舊虛擬 DOM 的差異,只針對差異部分進行實際 DOM 操作。
// 虛擬 DOM 概念示意
const vnode = {
tag: 'div',
props: { class: 'container' },
children: [
{ tag: 'h1', children: 'Hello' },
{ tag: 'p', children: 'World' },
],
};
2. 資料雙向綁定(Two-way Data Binding)
使用雙向資料綁定,當模型(Model)更改時,視圖(View)會自動更新,反之亦然。這讓開發者不需要手動操作 DOM,只需關注資料的變化。
<!-- Vue 3 推薦寫法:<script setup> -->
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('Hello Vue');
</script>
Options API 寫法
<template>
<input v-model="message" />
<p>{{ message }}</p>
</template>
<script>
export default {
data() {
return {
message: 'Hello Vue',
};
},
};
</script>
3. 組件化(Component-based)
將整個應用切分成一個個組件,意味著重用性提升,這對維護開發會更為省工。每個組件都有自己的狀態、樣式和邏輯,可以獨立開發和測試。
<!-- Button.vue - Vue 3 <script setup> -->
<template>
<button @click="handleClick">
<slot></slot>
</button>
</template>
<script setup>
const emit = defineEmits(['click']);
const handleClick = () => {
emit('click');
};
</script>
4. 生命週期(Lifecycle Hooks)
有自己的生命週期,當資料發生變化時,會觸發相應的生命週期鉤子,這樣就可以在特定的生命週期中,做出相應的操作。
<!-- Vue 3 <script setup> 寫法 -->
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue';
onMounted(() => {
// 組件掛載後執行
console.log('Component mounted!');
});
onUpdated(() => {
// 資料更新後執行
console.log('Component updated!');
});
onUnmounted(() => {
// 組件卸載後執行
console.log('Component unmounted!');
});
</script>
5. 指令系統(Directives)
提供了一些常用的指令,例如 v-if、v-for、v-bind、v-model 等,可以讓開發者更快速地開發。
<template>
<!-- 條件渲染 -->
<div v-if="isVisible">顯示內容</div>
<!-- 列表渲染 -->
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
<!-- 屬性綁定 -->
<img :src="imageUrl" :alt="imageAlt" />
<!-- 雙向綁定 -->
<input v-model="username" />
</template>
6. 模板語法(Template Syntax)
使用 template 來撰寫 HTML,允許將資料透過插值的方式,直接渲染到 template 中。
<template>
<div>
<!-- 文字插值 -->
<p>{{ message }}</p>
<!-- 表達式 -->
<p>{{ count + 1 }}</p>
<!-- 方法呼叫 -->
<p>{{ formatDate(date) }}</p>
</div>
</template>
Vue 的獨有優勢(和 React 相比)
1. 學習曲線較低
對團隊成員彼此程度的掌控落差不會太大,同時在書寫風格上,由官方統一規定,避免過於自由奔放,同時對不同專案的維護也能更快上手。
<!-- Vue 的單檔案組件結構清晰 -->
<template>
<!-- HTML 模板 -->
</template>
<script>
// JavaScript 邏輯
</script>
<style>
/* CSS 樣式 */
</style>
2. 擁有自己的獨特指令語法
雖然這點可能見仁見智,但 Vue 的指令系統提供了更直觀的方式來處理常見的 UI 邏輯:
<!-- Vue 指令 -->
<div v-if="isLoggedIn">歡迎回來</div>
<button @click="handleClick">點擊</button>
<!-- React JSX -->
<div>{isLoggedIn && '歡迎回來'}</div>
<button onClick="{handleClick}">點擊</button>
3. 資料雙向綁定更容易實現
因為有自己的指令,所以開發者實現資料雙向綁定可以非常容易(v-model),而 React 雖然也能實作類似的功能,但沒有 Vue 來得直覺。
<!-- Vue 雙向綁定 -->
<input v-model="username" />
<!-- React 需要手動處理 -->
<input value={username} onChange={(e) => setUsername(e.target.value)} />
4. 模板和邏輯分離
React 的 JSX 仍為部分開發者所詬病,在部分開發情境下,將邏輯和 UI 進行分離會顯得更易閱讀與維護。
<!-- Vue:結構清晰 -->
<template>
<div class="user-card">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: 'John',
email: 'john@example.com',
},
};
},
};
</script>
5. 官方生態系統完整
Vue 官方提供了完整的解決方案(Vue Router、Vuex/Pinia、Vue CLI),不需要在眾多第三方套件中選擇。
2. Please explain the usage of v-model, v-bind and v-html
請解釋
v-model、v-bind和v-html的使用方式
v-model:資料雙向綁定
當改變資料的同時,隨即驅動改變 template 上渲染的內容,反之改變 template 的內容,也會更新資料。
<template>
<div>
<!-- 文字輸入框 -->
<input v-model="message" />
<p>輸入的內容:{{ message }}</p>
<!-- 核取方塊 -->
<input type="checkbox" v-model="checked" />
<p>是否勾選:{{ checked }}</p>
<!-- 選項列表 -->
<select v-model="selected">
<option value="A">選項 A</option>
<option value="B">選項 B</option>
</select>
<p>選擇的選項:{{ selected }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: '',
checked: false,
selected: 'A',
};
},
};
</script>
v-model 的修飾符
<!-- .lazy:改為在 change 事件後更新 -->
<input v-model.lazy="msg" />
<!-- .number:自動轉為數字 -->
<input v-model.number="age" type="number" />
<!-- .trim:自動過濾首尾空白字元 -->
<input v-model.trim="msg" />
v-bind:動態綁定屬性
常見於綁定 class 或連結、圖片等。當透過 v-bind 綁定 class 後,可以透過資料變動,來決定該 class 樣式是否被綁定,同理 API 回傳的圖片路徑、連結網址,也能透過綁定的形式來維持動態更新。
<template>
<div>
<!-- 綁定 class(可以簡寫為 :class) -->
<div :class="{ active: isActive, 'text-danger': hasError }">動態 class</div>
<!-- 綁定 style -->
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">動態樣式</div>
<!-- 綁定圖片路徑 -->
<img :src="imageUrl" :alt="imageAlt" />
<!-- 綁定連結 -->
<a :href="linkUrl">前往連結</a>
<!-- 綁定自訂屬性 -->
<div :data-id="userId" :data-name="userName"></div>
</div>
</template>
<script>
export default {
data() {
return {
isActive: true,
hasError: false,
textColor: 'red',
fontSize: 16,
imageUrl: 'https://example.com/image.jpg',
imageAlt: '圖片描述',
linkUrl: 'https://example.com',
userId: 123,
userName: 'John',
};
},
};
</script>
v-bind 的簡寫
<!-- 完整寫法 -->
<img v-bind:src="imageUrl" />
<!-- 簡寫 -->
<img :src="imageUrl" />
<!-- 綁定多個屬性 -->
<div v-bind="objectOfAttrs"></div>
v-html:渲染 HTML 字串
如果資料回傳的內容中帶有 HTML 的標籤時,可以透過這個指令來渲染,例如顯示 Markdown 語法又或是對方直接回傳含有 <img> 標籤的圖片路徑。
<template>
<div>
<!-- 普通插值:會顯示 HTML 標籤 -->
<p>{{ rawHtml }}</p>
<!-- 輸出:<span style="color: red">紅色文字</span> -->
<!-- v-html:會渲染 HTML -->
<p v-html="rawHtml"></p>
<!-- 輸出:紅色文字(實際渲染為紅色) -->
</div>
</template>
<script>
export default {
data() {
return {
rawHtml: '<span style="color: red">紅色文字</span>',
};
},
};
</script>
⚠️ 安全性警告
千萬不要對使用者提供的內容使用 v-html,這會導致 XSS(跨站腳本攻擊)漏洞!
<!-- ❌ 危險:使用者可以注入惡意腳本 -->
<div v-html="userProvidedContent"></div>
<!-- ✅ 安全:只用於可信任的內容 -->
<div v-html="markdownRenderedContent"></div>
安全的替代方案
<template>
<div>
<!-- 使用套件進行 HTML 淨化 -->
<div v-html="sanitizedHtml"></div>
</div>
</template>
<script>
import DOMPurify from 'dompurify';
export default {
data() {
return {
userInput: '<img src=x onerror=alert("XSS")>',
};
},
computed: {
sanitizedHtml() {
// 使用 DOMPurify 清理 HTML
return DOMPurify.sanitize(this.userInput);
},
},
};
</script>
三者比較總結
| 指令 | 用途 | 簡寫 | 範例 |
|---|---|---|---|
v-model | 雙向綁定表單元素 | 無 | <input v-model="msg"> |
v-bind | 單向綁定屬性 | : | <img :src="url"> |
v-html | 渲染 HTML 字串 | 無 | <div v-html="html"></div> |
3. How to access HTML elements (Template Refs)?
Vue 若欲操作 HTML 元素,例如取得 input 元素並讓其聚焦 (focus) 該如何使用?
在 Vue 中,我們不建議使用 document.querySelector 來獲取 DOM 元素,而是使用 Template Refs。
Options API (Vue 2 / Vue 3)
使用 ref 屬性在模板中標記元素,然後透過 this.$refs 訪問。
<template>
<div>
<input ref="inputElement" />
<button @click="focusInput">Focus Input</button>
</div>
</template>
<script>
export default {
methods: {
focusInput() {
// 訪問 DOM 元素
this.$refs.inputElement.focus();
},
},
mounted() {
// 確保組件掛載後再訪問
console.log(this.$refs.inputElement);
},
};
</script>
Composition API (Vue 3)
在 <script setup> 中,我們宣告一個同名的 ref 變數來獲取元素。
<template>
<div>
<input ref="inputElement" />
<button @click="focusInput">Focus Input</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 1. 宣告一個與 template ref 同名的變數,初始值為 null
const inputElement = ref(null);
const focusInput = () => {
// 2. 透過 .value 訪問 DOM
inputElement.value?.focus();
};
onMounted(() => {
// 3. 確保組件掛載後再訪問
console.log(inputElement.value);
});
</script>
注意:
- 變數名稱必須與 template 中的
ref屬性值完全一致。 - 必須在組件掛載 (
onMounted) 後才能訪問到 DOM 元素,否則會是null。 - 如果是用在
v-for迴圈中,ref 會是一個陣列。
4. Please explain the difference between v-show and v-if
請解釋
v-show和v-if的區別
相同點(Similarities)
兩者都是用於操作 DOM 元素的顯示與隱藏,根據條件的不同,決定是否顯示內容。
<template>
<!-- 當 isVisible 為 true 時,都會顯示內容 -->
<div v-if="isVisible">使用 v-if</div>
<div v-show="isVisible">使用 v-show</div>
</template>
相異點(Differences)
1. DOM 操作方式不同
<template>
<div>
<!-- v-show:透過 CSS display 屬性控制 -->
<div v-show="false">這個元素仍存在於 DOM 中,只是 display: none</div>
<!-- v-if:直接從 DOM 中移除或新增 -->
<div v-if="false">這個元素不會出現在 DOM 中</div>
</div>
</template>
實際渲染結果:
<!-- v-show 渲染結果 -->
<div style="display: none;">這個元素仍存在於 DOM 中,只是 display: none</div>
<!-- v-if 渲染結果:false 時完全不存在 -->
<!-- 沒有任何 DOM 節點 -->
2. 效能差異
v-show:
- ✅ 初次渲染開銷較大(元素一定會被建立)
- ✅ 切換開銷較小(只改變 CSS)
- ✅ 適合頻繁切換的場景
v-if:
- ✅ 初次渲染開銷較小(條件為 false 時不渲染)
- ❌ 切換開銷較大(需要銷毀/重建元素)
- ✅ 適合條件不常改變的場景
<template>
<div>
<!-- 頻繁切換:使用 v-show -->
<button @click="toggleModal">切換彈窗</button>
<div v-show="showModal" class="modal">
彈窗內容(頻繁開關,使用 v-show 效能更好)
</div>
<!-- 不常切換:使用 v-if -->
<div v-if="userRole === 'admin'" class="admin-panel">
管理員面板(登入後幾乎不變,使用 v-if)
</div>
</div>
</template>
<script>
export default {
data() {
return {
showModal: false,
userRole: 'user',
};
},
methods: {
toggleModal() {
this.showModal = !this.showModal;
},
},
};
</script>
3. 生命週期觸發
v-if:
- 會觸發組件的完整生命週期
- 條件為 false 時,會執行
unmounted鉤子 - 條件為 true 時,會執行
mounted鉤子
<template>
<child-component v-if="showChild" />
</template>
<script>
// ChildComponent.vue
export default {
mounted() {
console.log('組件已掛載'); // v-if 從 false 變 true 時會執行
},
unmounted() {
console.log('組件已卸載'); // v-if 從 true 變 false 時會執行
},
};
</script>
v-show:
- 不會觸發組件的生命週期
- 組件始終保持掛載狀態
- 只是透過 CSS 隱藏
<template>
<child-component v-show="showChild" />
</template>
<script>
// ChildComponent.vue
export default {
mounted() {
console.log('組件已掛載'); // 只在第一次渲染時執行一次
},
unmounted() {
console.log('組件已卸載'); // 不會執行(除非父組件被銷毀)
},
};
</script>
4. 初始渲染成本
<template>
<div>
<!-- v-if:初始為 false 時,完全不渲染 -->
<heavy-component v-if="false" />
<!-- v-show:初始為 false 時,仍會渲染但隱藏 -->
<heavy-component v-show="false" />
</div>
</template>
如果 heavy-component 是一個很重的組件:
v-if="false":初始載入更快(不渲染)v-show="false":初始載入較慢(會渲染,只是隱藏)
5. 與其他指令搭配
v-if 可以搭配 v-else-if 和 v-else:
<template>
<div>
<div v-if="type === 'A'">類型 A</div>
<div v-else-if="type === 'B'">類型 B</div>
<div v-else>其他類型</div>
</div>
</template>
v-show 無法搭配 v-else:
<!-- ❌ 錯誤:v-show 不能使用 v-else -->
<div v-show="type === 'A'">類型 A</div>
<div v-else>其他類型</div>
<!-- ✅ 正確:需要分別設定條件 -->
<div v-show="type === 'A'">類型 A</div>
<div v-show="type !== 'A'">其他類型</div>
computed 與 watch 的使用建議
使用 v-if 的情境
- ✅ 條件很少改變
- ✅ 初始條件為 false,且可能永遠不會變成 true
- ✅ 需要配合
v-else-if或v-else使用 - ✅ 組件內有需要清理的資源(如計時器、事件監聽)
<template>
<!-- 權限控制:登入後幾乎不變 -->
<admin-panel v-if="isAdmin" />
<!-- 路由相關:頁面切換時才改變 -->
<home-page v-if="currentRoute === 'home'" />
<about-page v-else-if="currentRoute === 'about'" />
</template>
使用 v-show 的情境
- ✅ 需要頻繁切換顯示狀態
- ✅ 組件初始化成本高,希望保留狀態
- ✅ 不需要觸發生命週期鉤子
<template>
<!-- Tab 切換:使用者經常切換 -->
<div v-show="activeTab === 'profile'">個人資料</div>
<div v-show="activeTab === 'settings'">設定</div>
<!-- 彈窗:頻繁開關 -->
<modal v-show="isModalVisible" />
<!-- 載入動畫:頻繁顯示/隱藏 -->
<loading-spinner v-show="isLoading" />
</template>
效能比較總結
| 特性 | v-if | v-show |
|---|---|---|
| 初始渲染開銷 | 小(條件為 false 不渲染) | 大(一定會渲染) |
| 切換開銷 | 大(銷毀/重建元素) | 小(只改變 CSS) |
| 適用場景 | 條件不常改變 | 需要頻繁切換 |
| 生命週期 | 會觸發 | 不觸發 |
| 搭配使用 | v-else-if, v-else | 無 |
實際範例對比
<template>
<div>
<!-- 範例 1:管理員面板(使用 v-if) -->
<!-- 原因:登入後幾乎不變,且有權限控制 -->
<div v-if="userRole === 'admin'">
<h2>管理員面板</h2>
<button @click="deleteUser">刪除使用者</button>
</div>
<!-- 範例 2:彈窗(使用 v-show) -->
<!-- 原因:使用者會頻繁開關彈窗 -->
<div v-show="isModalOpen" class="modal">
<h2>彈窗標題</h2>
<p>彈窗內容</p>
<button @click="isModalOpen = false">關閉</button>
</div>
<!-- 範例 3:載入動畫(使用 v-show) -->
<!-- 原因:API 請求時會頻繁顯示/隱藏 -->
<div v-show="isLoading" class="loading">
<spinner />
</div>
<!-- 範例 4:錯誤訊息(使用 v-if) -->
<!-- 原因:不常出現,且出現時需要重新渲染 -->
<div v-if="errorMessage" class="error">
{{ errorMessage }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
userRole: 'user',
isModalOpen: false,
isLoading: false,
errorMessage: '',
};
},
};
</script>
v-if 與 v-show 記憶點
v-if:不顯示時就不渲染,適合不常改變的條件v-show:一開始就渲染好,隨時準備顯示,適合頻繁切換
5. What's the difference between computed and watch?
computed和watch有什麼差別?
這是 Vue 中兩個非常重要的響應式功能,雖然都能監聽資料變化,但使用場景和特性截然不同。
computed(計算屬性)
核心特性(computed)
- 緩存機制:
computed計算出來的結果會被緩存,只有當依賴的響應式資料改變時才會重新計算 - 自動追蹤依賴:會自動追蹤計算過程中使用到的響應式資料
- 同步計算:必須是同步操作,且必須有回傳值
- 簡潔的語法:可以直接在 template 中使用,如同 data 中的屬性
常見使用場景(computed)
<!-- Vue 3 <script setup> 寫法 -->
<template>
<div>
<!-- 範例 1:格式化資料 -->
<p>全名:{{ fullName }}</p>
<p>信箱:{{ emailLowerCase }}</p>
<!-- 範例 2:計算購物車總價 -->
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - ${{ item.price }} x {{ item.quantity }}
</li>
</ul>
<p>總計:${{ cartTotal }}</p>
<!-- 範例 3:過濾列表 -->
<input v-model="searchText" placeholder="搜尋..." />
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.name }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const email = ref('JOHN@EXAMPLE.COM');
const cart = ref([
{ id: 1, name: 'Apple', price: 2, quantity: 3 },
{ id: 2, name: 'Banana', price: 1, quantity: 5 },
]);
const searchText = ref('');
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' },
]);
// 範例 1:組合資料
const fullName = computed(() => {
console.log('計算 fullName'); // 只在依賴改變時才執行
return `${firstName.value} ${lastName.value}`;
});
// 範例 2:格式化資料
const emailLowerCase = computed(() => {
return email.value.toLowerCase();
});
// 範例 3:計算總價
const cartTotal = computed(() => {
console.log('計算 cartTotal'); // 只在 cart 改變時才執行
return cart.value.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
});
// 範例 4:過濾列表
const filteredItems = computed(() => {
if (!searchText.value) return items.value;
return items.value.filter((item) =>
item.name.toLowerCase().includes(searchText.value.toLowerCase())
);
});
</script>
computed 的優勢:緩存機制
<template>
<div>
<!-- 多次使用 computed,但只計算一次 -->
<p>{{ expensiveComputed }}</p>
<p>{{ expensiveComputed }}</p>
<p>{{ expensiveComputed }}</p>
<!-- 使用 method,每次都會重新計算 -->
<p>{{ expensiveMethod() }}</p>
<p>{{ expensiveMethod() }}</p>
<p>{{ expensiveMethod() }}</p>
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
const items = ref(Array.from({ length: 1000 }, (_, index) => index));
const expensiveComputed = computed(() => {
console.log('computed 執行'); // 只執行一次
return items.value.reduce((sum, item) => sum + item, 0);
});
const expensiveMethod = () => {
console.log('method 執行'); // 每次呼叫都會重新計算
return items.value.reduce((sum, item) => sum + item, 0);
};
</script>
computed 的 getter 和 setter
<script setup>
import { computed, onMounted, ref } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
// getter:讀取時執行
get() {
return `${firstName.value} ${lastName.value}`;
},
// setter:設定時執行
set(newValue) {
const names = newValue.split(' ');
firstName.value = names[0] ?? '';
lastName.value = names[names.length - 1] ?? '';
},
});
onMounted(() => {
console.log(fullName.value); // 'John Doe'(觸發 getter)
fullName.value = 'Jane Smith'; // 觸發 setter
console.log(firstName.value); // 'Jane'
console.log(lastName.value); // 'Smith'
});
</script>
watch(監聽屬性)
核心特性(watch)
- 手動追蹤資料變化:需要明確指定要監聽哪個資料
- 可執行非同步操作:適合呼叫 API、設定計時器等
- 不需要回傳值:主要用於執行副作用(side effects)
- 可以監聽多個資料:透過陣列或物件深度監聽
- 提供新舊值:可以拿到變化前後的值