Vue 3 Composition API: คู่มือฉบับสมบูรณ์เพื่อเชี่ยวชาญระบบ Reactivity
เชี่ยวชาญ Vue 3 Composition API ผ่านคู่มือเชิงปฏิบัตินี้ เรียนรู้ ref, reactive, computed, watch และ composables เพื่อสร้างแอปพลิเคชัน Vue ที่มีประสิทธิภาพสูง

Vue 3 Composition API ถือเป็นการเปลี่ยนแปลงครั้งสำคัญในวิธีการจัดโครงสร้างคอมโพเนนต์ Vue แนวทางนี้จัดระเบียบโค้ดตามฟีเจอร์แทนที่จะจัดตามออปชัน ช่วยให้การนำลอจิกกลับมาใช้ซ้ำและการดูแลแอปพลิเคชันที่ซับซ้อนทำได้ง่ายขึ้น
คู่มือนี้สมมติฐานว่าท่านมีความรู้พื้นฐานเกี่ยวกับ Vue.js ตัวอย่างใช้ไวยากรณ์ <script setup> ที่เปิดตัวใน Vue 3.2 ซึ่งปัจจุบันเป็นแนวทางที่แนะนำ
ทำความเข้าใจ Reactivity ด้วย ref และ reactive
Reactivity คือหัวใจสำคัญของ Vue 3 Primitive หลักสองตัวสำหรับสร้างข้อมูลแบบ reactive คือ: ref สำหรับค่าพื้นฐาน และ reactive สำหรับอ็อบเจกต์ที่ซับซ้อน
ฟังก์ชัน ref สร้างการอ้างอิงแบบ reactive ที่ห่อหุ้มค่าไว้ หากต้องการเข้าถึงหรือเปลี่ยนแปลงค่านี้ใน script ต้องใช้พร็อพเพอร์ตี้ .value แต่ใน template Vue จะทำการ unwrap พร็อปเพอร์ตี้นี้ให้โดยอัตโนมัติ
<script setup>
import { ref } from 'vue'
// Create a ref with initial value 0
const count = ref(0)
// Function that increments the counter
// Note: .value is required in the script
const increment = () => {
count.value++
}
// Reset function
const reset = () => {
count.value = 0
}
</script>
<template>
<!-- In the template, no .value needed -->
<div class="counter">
<p>Counter: {{ count }}</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
</template>สำหรับอ็อบเจกต์ที่มีพร็อปเพอร์ตี้หลายตัวที่เกี่ยวข้องกัน reactive มอบไวยากรณ์ที่เป็นธรรมชาติมากขึ้นโดยไม่ต้องใช้ .value
<script setup>
import { reactive } from 'vue'
// reactive for complex objects
// All properties are automatically reactive
const user = reactive({
name: 'Jane Smith',
email: 'jane@example.com',
preferences: {
theme: 'dark',
notifications: true
}
})
// Direct property modification (no .value)
const updateTheme = (newTheme) => {
user.preferences.theme = newTheme
}
// Warning: do not reassign the entire object
// user = { ... } would break reactivity
</script>
<template>
<div>
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<p>Theme: {{ user.preferences.theme }}</p>
</div>
</template>กฎทั่วไปคือ: ใช้ ref สำหรับค่าพื้นฐาน (string, number, boolean) และ reactive สำหรับอ็อบเจกต์ที่มีโครงสร้าง
พร็อปเพอร์ตี้ Computed ด้วย computed
พร็อปเพอร์ตี้ computed คำนวณค่าจาก state แบบ reactive ค่าเหล่านี้จะถูกแคชไว้และคำนวณใหม่เฉพาะเมื่อ dependency เปลี่ยนแปลงเท่านั้น จึงมีประสิทธิภาพสูงมาก
<script setup>
import { ref, computed } from 'vue'
// Product list
const products = ref([
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Mouse', price: 29, inStock: true },
{ id: 3, name: 'Keyboard', price: 79, inStock: false },
{ id: 4, name: 'Monitor', price: 299, inStock: true }
])
// Active filter
const showOnlyInStock = ref(false)
// computed: automatic filtering based on toggle
// Only recalculates if products or showOnlyInStock changes
const filteredProducts = computed(() => {
if (showOnlyInStock.value) {
return products.value.filter(p => p.inStock)
}
return products.value
})
// computed: total calculation with caching
const totalValue = computed(() => {
return filteredProducts.value.reduce((sum, p) => sum + p.price, 0)
})
// computed: number of displayed items
const productCount = computed(() => filteredProducts.value.length)
</script>
<template>
<div>
<label>
<input type="checkbox" v-model="showOnlyInStock" />
Show only in-stock products
</label>
<p>{{ productCount }} products - Total: ${{ totalValue }}</p>
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</div>
</template>พร็อปเพอร์ตี้ computed ยังสามารถเขียนได้ (writable) โดยใช้ getter และ setter ซึ่งมีประโยชน์สำหรับการแปลงค่าสองทิศทาง
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// computed with getter AND setter
const fullName = computed({
// Read: combine first and last name
get() {
return `${firstName.value} ${lastName.value}`
},
// Write: split the string into first/last name
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts.slice(1).join(' ') || ''
}
})
</script>
<template>
<!-- Modifying fullName updates firstName and lastName -->
<input v-model="fullName" placeholder="Full name" />
<p>First name: {{ firstName }}</p>
<p>Last name: {{ lastName }}</p>
</template>Watcher ด้วย watch และ watchEffect
Watcher ทำหน้าที่รัน side effect เพื่อตอบสนองต่อการเปลี่ยนแปลงของข้อมูล Vue 3 มีสองแนวทาง: watch สำหรับการควบคุมที่แม่นยำ และ watchEffect สำหรับการติดตามอัตโนมัติ
watch ให้การควบคุม dependency ที่แม่นยำและให้ทั้งค่าเก่าและค่าใหม่ watchEffect เหมาะกว่าเมื่อ dependency แบบ reactive ทั้งหมดที่ใช้ควรทริกเอฟเฟกต์
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const searchResults = ref([])
const isLoading = ref(false)
// watch: observes a specific source
// Provides oldValue and newValue, allows debouncing
watch(searchQuery, async (newQuery, oldQuery) => {
console.log(`Search changed from "${oldQuery}" to "${newQuery}"`)
// Don't search if less than 3 characters
if (newQuery.length < 3) {
searchResults.value = []
return
}
isLoading.value = true
try {
// Simulated API call
const response = await fetch(`/api/search?q=${newQuery}`)
searchResults.value = await response.json()
} finally {
isLoading.value = false
}
}, {
// Watcher options
debounce: 300, // Wait 300ms after last keystroke
immediate: false // Don't execute immediately
})
// watchEffect: automatic dependency tracking
// Runs immediately and on every change
watchEffect(() => {
// All refs used here are automatically tracked
console.log(`Current state: ${searchResults.value.length} results`)
console.log(`Loading: ${isLoading.value}`)
})
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="Search..." />
<p v-if="isLoading">Loading...</p>
<ul v-else>
<li v-for="result in searchResults" :key="result.id">
{{ result.title }}
</li>
</ul>
</div>
</template>หากต้องการสังเกตอ็อบเจกต์หรืออาร์เรย์ที่ซ้อนกัน ตัวเลือก deep จำเป็นต้องใช้กับ watch
<script setup>
import { reactive, watch } from 'vue'
const settings = reactive({
display: {
theme: 'light',
fontSize: 14
},
notifications: {
email: true,
push: false
}
})
// deep: true to observe nested changes
watch(
() => settings.display,
(newDisplay) => {
console.log('Display settings changed:', newDisplay)
// Save to localStorage for example
localStorage.setItem('display', JSON.stringify(newDisplay))
},
{ deep: true }
)
</script>พร้อมที่จะพิชิตการสัมภาษณ์ Vue.js / Nuxt.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การสร้าง Composable ที่นำกลับมาใช้ซ้ำได้
Composable คือฟังก์ชันที่ห่อหุ้มลอจิกแบบ reactive ที่นำกลับมาใช้ซ้ำได้ แนวทางนี้เป็นหนึ่งในจุดแข็งหลักของ Composition API สำหรับการแชร์โค้ดระหว่างคอมโพเนนต์
import { ref, watchEffect, toValue } from 'vue'
// Composable for HTTP requests
// Automatically handles loading, errors, and refetching
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
// Reusable fetch function
async function fetchData() {
isLoading.value = true
error.value = null
try {
// toValue() allows accepting a ref or a value
const response = await fetch(toValue(url))
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
data.value = await response.json()
} catch (e) {
error.value = e.message
} finally {
isLoading.value = false
}
}
// watchEffect for automatic refetch if url is a ref
watchEffect(() => {
fetchData()
})
// Expose state and methods
return {
data,
error,
isLoading,
refetch: fetchData
}
}Composable นี้สามารถนำไปใช้แบบ declarative ในคอมโพเนนต์ใดก็ได้
<script setup>
import { useFetch } from '@/composables/useFetch'
// Simple composable usage
const { data: users, isLoading, error, refetch } = useFetch('/api/users')
</script>
<template>
<div>
<button @click="refetch" :disabled="isLoading">
Refresh
</button>
<p v-if="isLoading">Loading users...</p>
<p v-else-if="error" class="error">Error: {{ error }}</p>
<ul v-else-if="users">
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</template>ต่อไปนี้คือตัวอย่าง composable อีกตัวสำหรับจัดการสถานะฟอร์มพร้อม validation
import { reactive, computed } from 'vue'
// Form management composable
export function useForm(initialValues, validationRules) {
// Form state
const form = reactive({
values: { ...initialValues },
errors: {},
touched: {}
})
// Validate a specific field
const validateField = (field) => {
const rules = validationRules[field]
if (!rules) return true
for (const rule of rules) {
const result = rule(form.values[field])
if (result !== true) {
form.errors[field] = result
return false
}
}
form.errors[field] = null
return true
}
// Validate entire form
const validate = () => {
let isValid = true
for (const field in validationRules) {
if (!validateField(field)) {
isValid = false
}
}
return isValid
}
// computed: form is valid if no errors
const isValid = computed(() => {
return Object.values(form.errors).every(e => !e)
})
// Mark a field as touched (to display errors)
const touch = (field) => {
form.touched[field] = true
validateField(field)
}
// Reset the form
const reset = () => {
form.values = { ...initialValues }
form.errors = {}
form.touched = {}
}
return {
form,
isValid,
validate,
validateField,
touch,
reset
}
}การจัดการ Lifecycle Hook
Composition API มอบ lifecycle hook ในรูปแบบฟังก์ชันที่เรียกใช้ภายใน setup Hook เหล่านี้ทำงานโค้ดในช่วงเวลาเฉพาะของวงจรชีวิตคอมโพเนนต์
ทำความสะอาด side effect (ไทเมอร์, event listener, subscription) ใน onUnmounted เสมอเพื่อป้องกันการรั่วไหลของหน่วยความจำ
<script setup>
import {
ref,
onMounted,
onUnmounted,
onBeforeUpdate,
onUpdated
} from 'vue'
const windowWidth = ref(window.innerWidth)
const updateCount = ref(0)
// Handler for resize
const handleResize = () => {
windowWidth.value = window.innerWidth
}
// onMounted: component inserted into DOM
// Ideal for initial API calls and event listeners
onMounted(() => {
console.log('Component mounted')
window.addEventListener('resize', handleResize)
// Example: initialize a third-party library
// chart = new Chart(chartRef.value, config)
})
// onUnmounted: component removed from DOM
// CRITICAL: clean up all side effects
onUnmounted(() => {
console.log('Component unmounted')
window.removeEventListener('resize', handleResize)
// Clean up subscriptions, timers, etc.
// chart?.destroy()
})
// onBeforeUpdate: before DOM update
onBeforeUpdate(() => {
console.log('Before update')
})
// onUpdated: after DOM update
onUpdated(() => {
updateCount.value++
console.log(`DOM updated (${updateCount.value} times)`)
})
</script>
<template>
<div>
<p>Window width: {{ windowWidth }}px</p>
<p>DOM updates: {{ updateCount }}</p>
</div>
</template>Template Ref และการเข้าถึง DOM
Template ref ให้สิทธิ์เข้าถึงเอลิเมนต์ DOM หรือ instance ของคอมโพเนนต์ลูกโดยตรง คุณสมบัตินี้ยังคงมีประโยชน์สำหรับกรณีที่ต้องการจัดการโดยตรง
<script setup>
import { ref, onMounted } from 'vue'
// ref for the DOM element
// Same name as the ref attribute in the template
const inputRef = ref(null)
// Function to focus the input
const focusInput = () => {
// Access to the native DOM element
inputRef.value?.focus()
}
// Auto-focus on mount
onMounted(() => {
focusInput()
})
// Expose the method to parent if needed
defineExpose({
focus: focusInput
})
</script>
<template>
<div>
<!-- The ref attribute binds the element to inputRef -->
<input ref="inputRef" type="text" placeholder="Auto-focused" />
<button @click="focusInput">Refocus</button>
</div>
</template>การสื่อสารระหว่าง Parent-Child ด้วย Props และ Emits
Composition API ทำให้การประกาศ props และ event ทันสมัยยิ่งขึ้นด้วย defineProps และ defineEmits พร้อมการทำงานร่วมกับ TypeScript ที่ดีขึ้น
<script setup>
// defineProps: declares expected props
// With default values via withDefaults
const props = withDefaults(defineProps<{
title: string
count?: number
items?: string[]
}>(), {
count: 0,
items: () => []
})
// defineEmits: declares emitted events
// Precise payload typing
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'submit', data: { title: string; items: string[] }): void
}>()
// Function that emits an event
const handleSubmit = () => {
emit('submit', {
title: props.title,
items: props.items
})
}
const increment = () => {
emit('update', props.count + 1)
}
</script>
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="handleSubmit">Submit</button>
</div>
</template><script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const currentCount = ref(0)
const items = ref(['Item 1', 'Item 2'])
// Handler for update event
const onUpdate = (newValue) => {
currentCount.value = newValue
}
// Handler for submit event
const onSubmit = (data) => {
console.log('Data submitted:', data)
}
</script>
<template>
<ChildComponent
title="My Component"
:count="currentCount"
:items="items"
@update="onUpdate"
@submit="onSubmit"
/>
</template>พร้อมที่จะพิชิตการสัมภาษณ์ Vue.js / Nuxt.js แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
Provide และ Inject สำหรับ Dependency Injection
หากต้องการแชร์ข้อมูลระหว่างคอมโพเนนต์ที่อยู่ห่างไกลกันโดยไม่ต้อง prop drilling Vue 3 มอบ provide และ inject ให้
<script setup>
import { provide, ref, readonly } from 'vue'
// Global theme state
const theme = ref('light')
// Function to toggle theme
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
// provide: makes available to descendants
// readonly prevents direct modification by children
provide('theme', readonly(theme))
provide('toggleTheme', toggleTheme)
</script>
<template>
<div :class="theme">
<slot />
</div>
</template><script setup>
import { inject } from 'vue'
// inject: retrieves value provided by an ancestor
// Default value if not found
const theme = inject('theme', 'light')
const toggleTheme = inject('toggleTheme', () => {})
</script>
<template>
<div>
<p>Current theme: {{ theme }}</p>
<button @click="toggleTheme">
Switch to {{ theme === 'light' ? 'dark' : 'light' }}
</button>
</div>
</template>สรุป
Vue 3 Composition API มอบแนวทางที่ทรงพลังและยืดหยุ่นสำหรับการจัดระเบียบโค้ดแอปพลิเคชัน Vue แนวคิดหลักที่ควรจดจำ:
- ref สำหรับค่าพื้นฐาน, reactive สำหรับอ็อบเจกต์ที่ซับซ้อน
- computed สำหรับค่าที่คำนวณได้พร้อมการแคชอัตโนมัติ
- watch และ watchEffect สำหรับ side effect แบบ reactive
- Composable สำหรับแยกและนำลอจิกกลับมาใช้ซ้ำระหว่างคอมโพเนนต์
- Lifecycle hook แบบฟังก์ชัน (
onMounted,onUnmountedเป็นต้น) - provide/inject สำหรับ dependency injection โดยไม่ต้อง prop drilling
แนวทางนี้ช่วยให้การสร้างแอปพลิเคชันที่ดูแลง่ายและทดสอบได้สะดวก พร้อมกับการทำงานร่วมกับ TypeScript ได้อย่างยอดเยี่ยม ขั้นตอนต่อไปคือการศึกษารูปแบบขั้นสูง เช่น async composable และการใช้งานร่วมกับ Pinia สำหรับการจัดการ state ระดับโกลบอล
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
