☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
vue 進階

⚡ 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 開頭(useMouseuseFetch
  • provide/inject 適合「主題」「語系」等全域設定,不要濫用
  • 虛擬 DOM 的存在就是因為直接操作 DOM 太慢——框架用 JS 物件做差異比較
  • 效能優化遵循「先量測、再優化」——不要過早優化

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊