⚡ Vue 進階:組合式函式與效能優化
📌 自訂 Composable (useXxx)
Composable 是 Vue 3 最強大的程式碼復用模式——把可重用的邏輯抽成函式。
Composable 的本質就是 JavaScript 函式! 只是裡面使用了 Vue 的響應式 API。
範例:useMouse — 追蹤滑鼠位置
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event) {
// event 是原生 DOM 事件(這是 JS 原生的!)
x.value = event.clientX
y.value = event.clientY
}
// onMounted / onUnmounted 是 Vue 的生命週期鉤子
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
<!-- 在元件中使用 -->
<template>
<p>滑鼠位置:{{ x }}, {{ y }}</p>
</template>
<script setup>
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
範例:useFetch — 封裝 API 請求
// composables/useFetch.js
import { ref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(true)
async function fetchData() {
loading.value = true
error.value = null
try {
// fetch 是瀏覽器原生 API,不是 Vue 的
const response = await fetch(url.value || url)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
// watchEffect 自動追蹤響應式依賴
watchEffect(() => {
fetchData()
})
return { data, error, loading, refetch: fetchData }
}
<template>
<div v-if="loading">載入中...</div>
<div v-else-if="error">錯誤:{{ error }}</div>
<div v-else>
<pre>{{ data }}</pre>
</div>
<button @click="refetch">重新載入</button>
</template>
<script setup>
import { useFetch } from '@/composables/useFetch'
const { data, error, loading, refetch } = useFetch('https://api.example.com/data')
</script>
範例:useLocalStorage — 持久化響應式資料
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
// localStorage 是瀏覽器原生 API
const stored = localStorage.getItem(key)
const data = ref(stored ? JSON.parse(stored) : defaultValue)
// 當資料變化時自動存到 localStorage
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return data
}
📌 watchEffect vs watch
<script setup>
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
const name = ref('小明')
// watchEffect:自動追蹤用到的所有響應式資料
// 立即執行一次,之後只要依賴變化就再執行
watchEffect(() => {
console.log(`count = ${count.value}`)
// 只要 count 變了就會觸發
// 如果裡面也用了 name.value,name 變了也會觸發
})
// watch:明確指定要監聽的來源
// 預設不立即執行
watch(count, (newVal, oldVal) => {
console.log(`count 從 ${oldVal} 變成 ${newVal}`)
})
// watch 多個來源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} → ${newCount}`)
console.log(`name: ${oldName} → ${newName}`)
})
// watch 深層物件
const user = ref({ name: '小明', address: { city: '台北' } })
watch(
() => user.value.address.city,
(newCity) => {
console.log(`搬到 ${newCity} 了`)
}
)
</script>
| 比較 | watch |
watchEffect |
|---|---|---|
| 指定來源 | 明確指定 | 自動追蹤 |
| 立即執行 | 預設否 | 預設是 |
| 取得舊值 | ✅ (new, old) |
❌ |
| 適合場景 | 需要比較新舊值 | 副作用同步 |
📌 provide / inject — 依賴注入
跨越多層元件傳遞資料,不用一層層 props。
<!-- 祖先元件 -->
<script setup>
import { ref, provide } from 'vue'
const theme = ref('dark')
const toggleTheme = () => {
theme.value = theme.value === 'dark' ? 'light' : 'dark'
}
// provide 提供資料給所有後代元件
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
<!-- 深層子元件(不管隔幾層都能用) -->
<script setup>
import { inject } from 'vue'
// inject 接收祖先 provide 的資料
const theme = inject('theme') // ref('dark')
const toggleTheme = inject('toggleTheme') // function
// 可以設定預設值
const locale = inject('locale', 'zh-TW')
</script>
📌 虛擬 DOM 與 Diff 算法原理
這裡揭示框架的底層——全部都是 JavaScript!
什麼是虛擬 DOM?
// 真實 DOM(瀏覽器的東西,操作很慢)
const div = document.createElement('div')
div.textContent = 'Hello'
document.body.appendChild(div) // 觸發瀏覽器重排 (reflow)
// 虛擬 DOM(就是普通的 JavaScript 物件!)
const vnode = {
tag: 'div',
props: { class: 'greeting' },
children: 'Hello'
}
// 這只是一個 JS 物件,操作它不會觸發瀏覽器重排
// Vue 在背後比較新舊 vnode,只更新有變化的部分
Diff 算法簡化版
// Vue 的 diff 算法(簡化概念)
function patch(oldVNode, newVNode) {
// 1. 如果節點類型不同 → 直接替換
if (oldVNode.tag !== newVNode.tag) {
replaceNode(oldVNode, newVNode)
return
}
// 2. 更新屬性(只改變化的部分)
updateProps(oldVNode.props, newVNode.props)
// 3. 比較子節點(這是最複雜的部分)
diffChildren(oldVNode.children, newVNode.children)
}
// 這就是為什麼 v-for 需要 :key
// key 幫助 Vue 識別哪些節點可以復用,避免不必要的 DOM 操作
📌 效能優化技巧
v-memo — 跳過不必要的更新
<template>
<!-- v-memo 會記住渲染結果,只有依賴變化才重新渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.name, item.selected]">
<!-- 只有 item.name 或 item.selected 變化時才重新渲染這個 div -->
<p>{{ item.name }}</p>
<p>{{ formatDate(item.updatedAt) }}</p>
</div>
</template>
shallowRef — 淺層響應式
import { shallowRef, triggerRef } from 'vue'
// shallowRef 只追蹤 .value 本身的變化
// 不追蹤深層屬性(適合大型物件/陣列)
const bigList = shallowRef([])
// ❌ 這不會觸發更新(淺層追蹤)
bigList.value.push({ name: 'new' })
// ✅ 替換整個陣列才會觸發
bigList.value = [...bigList.value, { name: 'new' }]
// ✅ 或手動觸發更新
bigList.value.push({ name: 'new' })
triggerRef(bigList)
computed 快取
import { computed, ref } from 'vue'
const items = ref([/* 1000 個商品 */])
// ✅ computed 有快取:只在 items 變化時重新計算
const expensiveItems = computed(() => {
console.log('重新計算!')
return items.value
.filter(item => item.price > 1000)
.sort((a, b) => b.price - a.price)
})
// ❌ 不要用方法代替,每次渲染都會重新執行
function getExpensiveItems() {
return items.value.filter(item => item.price > 1000)
}
📌 打包優化與 Tree Shaking
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
// 分割程式碼
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-lib': ['element-plus']
}
}
},
// 壓縮
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console.log
drop_debugger: true // 移除 debugger
}
}
}
})
Tree Shaking
// ✅ 按需引入(Tree Shaking 友好)
import { ref, computed, watch } from 'vue'
// ❌ 全部引入(打包會包含 Vue 所有功能)
import * as Vue from 'vue'
💡 進階提醒
- Composable 命名慣例:以
use開頭(useMouse、useFetch) provide/inject適合「主題」「語系」等全域設定,不要濫用- 虛擬 DOM 的存在就是因為直接操作 DOM 太慢——框架用 JS 物件做差異比較
- 效能優化遵循「先量測、再優化」——不要過早優化