Vue 3 Composition API: คู่มือฉบับสมบูรณ์เพื่อเชี่ยวชาญระบบ Reactivity

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

ภาพประกอบ Vue 3 Composition API พร้อมบล็อกโค้ด reactive ที่เชื่อมต่อกัน

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 พร็อปเพอร์ตี้นี้ให้โดยอัตโนมัติ

Counter.vuejavascript
<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

UserProfile.vuejavascript
<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 เปลี่ยนแปลงเท่านั้น จึงมีประสิทธิภาพสูงมาก

ProductList.vuejavascript
<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 ซึ่งมีประโยชน์สำหรับการแปลงค่าสองทิศทาง

FullName.vuejavascript
<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 และเมื่อไหร่ควรใช้ watchEffect?

watch ให้การควบคุม dependency ที่แม่นยำและให้ทั้งค่าเก่าและค่าใหม่ watchEffect เหมาะกว่าเมื่อ dependency แบบ reactive ทั้งหมดที่ใช้ควรทริกเอฟเฟกต์

SearchComponent.vuejavascript
<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

DeepWatch.vuejavascript
<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 สำหรับการแชร์โค้ดระหว่างคอมโพเนนต์

composables/useFetch.jsjavascript
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 ในคอมโพเนนต์ใดก็ได้

UserList.vuejavascript
<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

composables/useForm.jsjavascript
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 เสมอเพื่อป้องกันการรั่วไหลของหน่วยความจำ

LifecycleDemo.vuejavascript
<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 ของคอมโพเนนต์ลูกโดยตรง คุณสมบัตินี้ยังคงมีประโยชน์สำหรับกรณีที่ต้องการจัดการโดยตรง

InputFocus.vuejavascript
<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 ที่ดีขึ้น

ChildComponent.vuejavascript
<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>
ParentComponent.vuejavascript
<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 ให้

App.vue (or an ancestor component)javascript
<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>
DeepNestedComponent.vue (anywhere in the tree)javascript
<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 ระดับโกลบอล

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

แท็ก

#vue 3
#composition api
#javascript
#frontend
#reactivity

แชร์

บทความที่เกี่ยวข้อง