Vue 3 實戰:從零到一封裝一個 useMouse Composable
在 Vue 3 中,將元件邏輯 去中心化 是保持程式碼整潔的關鍵。今天我們以常見的 滑鼠座標監聽 為例,示範如何從傳統的元件件寫法,重構成靈活的 Composable 函數。
初始狀態:在單一元件 (SFC) 中撰寫
通常我們會直接在 App.vue 寫入監聽邏輯。雖然功能正常,但若其他元件也需要同樣的功能,就必須複製貼上,這違反了 DRY (Don't Repeat Yourself) 原則。
// App.vue
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => {
window.addEventListener("mousemove", update);
});
onUnmounted(() => {
window.removeEventListener("mousemove", update);
});
</script>
<template>
<p>Mouse Position: {{ x }}, {{ y }}</p>
</template>這裡有兩個容易被忽略的細節:
update必須是具名函式,因為removeEventListener要傳入與註冊時相同的函式參考,寫成匿名箭頭函式就永遠移不掉。- 用
pageX而非clientX,代表座標 包含頁面捲動距離。跟隨畫面的浮動提示則適合改用clientX。
進階重構:封裝成可重複使用的邏輯函數
為了達到多元件共享,我們根據 Composable 的設計規範,在 composables 目錄下建立專屬檔案,並以 use 開頭命名。
實作 useMouse.js
核心邏輯保持不變,但我們將狀態與行為封裝在函數中,並 回傳 需要暴露給外部使用的響應式屬性。
// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export default () => {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return { x, y };
};生命週期為什麼能寫在這裡?
因為這個函式是在元件的 setup 中 同步呼叫 的,Vue 知道 onMounted 屬於哪個元件。而且同一個鉤子 可以註冊多次,所以一個元件同時使用多個 Composable,各自的建立與清理都不會互相干擾。
最終成果:在元件中優雅呼叫
重構後的元件變得極度簡潔,所有的複雜邏輯都被抽離到 useMouse 之中,元件只負責讀取數據與渲染 UI。
// App.vue
<script setup>
import useMouse from "@/composables/useMouse";
const { x, y } = useMouse();
</script>
<template>
<p>Mouse Position: {{ x }}, {{ y }}</p>
</template>核心要點筆記
獨立狀態
每一個引用 useMouse 的元件,都會擁有自己獨立的 x 與 y 狀態。即使在不同元件中同時使用,數據也不會相互干擾或衝突。
若希望 全站共用一份座標,只要把 ref 移到函式外層,所有呼叫者就會共享同一組狀態,也只需要一組事件監聽:
// 模組層級的狀態:所有元件共用
const x = ref(0);
const y = ref(0);
export default () => {
/* 註冊監聽並回傳同一份 x、y */
};清理機制
將 onUnmounted 封裝在 Composable 內部,確保了當元件銷毀時,監聽器會自動移除,有效防止記憶體洩漏 (Memory Leak)。
這正是把 建立與清理寫在一起 的價值:呼叫端不需要知道內部註冊了什麼,也不會有人忘記解除監聽。相關的清理原則可參考 生命週期實戰。
高可讀性
透過解構賦值,我們可以一眼看出元件使用了哪些外部邏輯,提升了大型專案的可維護性。
效能:高頻事件要不要節流?
mousemove 每秒可能觸發數十次。若只是更新兩個數值,Vue 的響應式系統完全負擔得起;但如果在回呼中做計算或直接操作 DOM,建議把更新收斂到畫面重繪的節奏上:
let rafId = null;
function update(e) {
if (rafId) return;
rafId = requestAnimationFrame(() => {
x.value = e.pageX;
y.value = e.pageY;
rafId = null;
});
}記得在 onUnmounted 一併 cancelAnimationFrame(rafId),清理才算完整。
常見問題
為什麼可以把 onMounted 寫在 Composable 裡?
因為 Composable 是在元件的 setup 中同步呼叫的,Vue 能判斷這些鉤子屬於哪個元件。同一個鉤子也允許註冊多次,所以多個 Composable 各自管理自己的建立與清理不會衝突。
多個元件同時使用 useMouse 會互相影響嗎?
不會。每次呼叫都會建立獨立的 x 與 y,也各自註冊一組事件監聽。若希望全站共用一份座標,可以把狀態移到函式外層變成共享狀態。
為什麼 update 要寫成具名函式?
因為 removeEventListener 必須傳入與註冊時相同的函式參考才能解除。若寫成匿名箭頭函式,卸載時就再也找不到那個監聽器。
pageX 與 clientX 有什麼不同?
pageX 是相對於整份文件的座標,會把頁面捲動距離算進去;clientX 是相對於可視區域。需要在捲動後仍然定位正確就用 pageX,跟隨畫面的浮動提示則用 clientX。
mousemove 觸發太頻繁會有效能問題嗎?
單純更新兩個數值通常沒問題。但若在回呼中做計算或操作 DOM,建議搭配節流或 requestAnimationFrame,把更新頻率控制在畫面重繪的節奏上。