คำถามสัมภาษณ์ Vue.js: 25 ข้อสำคัญเพื่อคว้างาน

เตรียมสัมภาษณ์ Vue.js ด้วย 25 คำถามสำคัญ ตั้งแต่ระบบ reactivity ถึง composables เพื่อพิชิตการสัมภาษณ์ครั้งต่อไป

ภาพประกอบการสัมภาษณ์เทคนิค Vue.js พร้อมบล็อกโค้ดและโลโก้ Vue

การสัมภาษณ์ Vue.js วัดผลมากกว่าแค่ไวยากรณ์ของเฟรมเวิร์ก ผู้สรรหาต้องการเห็นความเข้าใจระบบ reactivity ความสามารถในการจัดระเบียบโค้ดด้วย Composition API และทักษะการแก้ปัญหาด้านประสิทธิภาพและสถาปัตยกรรมที่เกิดขึ้นจริง

คำแนะนำในการเตรียมตัว

ทุกคำถามมาพร้อมคำตอบโดยละเอียดและตัวอย่างโค้ด สำหรับการสัมภาษณ์ทางเทคนิค การฝึกอธิบายแนวคิดออกเสียงเหมือนอยู่ในห้องสัมภาษณ์จริงจะช่วยได้มาก

คำถามพื้นฐานเกี่ยวกับ Vue.js

1. ref และ reactive ใน Vue 3 ต่างกันอย่างไร

คำถามนี้ทดสอบความเข้าใจระบบ reactivity ซึ่งเป็นหัวใจของ Vue 3 ความแตกต่างหลักอยู่ที่ประเภทข้อมูลและรูปแบบการเข้าถึง

ref สร้างการอ้างอิงแบบ reactive สำหรับค่าพื้นฐาน (string, number, boolean) และต้องใช้ .value เพื่อเข้าถึงค่าภายในสคริปต์ ส่วน reactive สร้าง proxy แบบ reactive สำหรับ object และเข้าถึงคุณสมบัติได้โดยตรง

javascript
// ตัวอย่างเปรียบเทียบ ref กับ reactive
import { ref, reactive } from 'vue'

// ref: for primitives
// Requires .value in the script
const count = ref(0)
count.value++ // Access with .value

// reactive: for complex objects
// Direct property access
const user = reactive({
  name: 'Alice',
  age: 25
})
user.age++ // No .value needed

// Warning: reactive loses reactivity if reassigned
// user = { name: 'Bob', age: 30 } // ❌ Breaks reactivity
Object.assign(user, { name: 'Bob', age: 30 }) // ✅ Correct

หลักทั่วไป: ใช้ ref กับค่าธรรมดา และ reactive กับ object ที่มีหลายคุณสมบัติเชื่อมโยงกัน

2. ระบบ reactivity ของ Vue 3 ทำงานอย่างไร

Vue 3 ใช้ Proxy ของ JavaScript (ES6) เพื่อดักจับการดำเนินการบน object แบบ reactive ต่างจาก Vue 2 ที่ใช้ Object.defineProperty วิธีนี้สามารถตรวจจับการเพิ่มและลบคุณสมบัติแบบไดนามิก

javascript
// Simplified demonstration of the reactivity principle
// Vue uses Proxies to track dependencies

const handler = {
  // Intercept reading
  get(target, key, receiver) {
    track(target, key) // Register the dependency
    return Reflect.get(target, key, receiver)
  },
  // Intercept writing
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver)
    trigger(target, key) // Trigger updates
    return result
  }
}

// Creating a reactive proxy
const reactiveObject = new Proxy(originalObject, handler)

ข้อดีของ Proxy ได้แก่ การตรวจจับคุณสมบัติใหม่ การรองรับ Map และ Set รวมถึงประสิทธิภาพที่ดีขึ้นกับ object ขนาดใหญ่

3. อธิบายความต่างระหว่าง computed และ watch

computed และ watch ตอบโจทย์การจัดการ reactivity ที่ต่างกัน

Computed: คำนวณค่าที่เป็นผลจากข้อมูล reactive อื่น ค่าจะถูก cache ไว้และคำนวณใหม่เฉพาะตอนที่ dependency เปลี่ยน เหมาะกับการแปลงข้อมูล

Watch: รัน side effect เพื่อตอบสนองต่อการเปลี่ยนแปลง ใช้สำหรับการเรียก API การโต้ตอบกับ DOM หรือการดำเนินการแบบอะซิงโครนัส

javascript
// Computed vs watch comparison
import { ref, computed, watch } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// computed: derived value with cache
// Recalculates only if firstName or lastName changes
const fullName = computed(() => {
  console.log('Computing full name') // Called only once
  return `${firstName.value} ${lastName.value}`
})

// Multiple accesses = single execution (cached)
console.log(fullName.value) // "John Doe"
console.log(fullName.value) // No recalculation

// watch: side effect without cache
// Executed on every change
watch(firstName, async (newName, oldName) => {
  // Side effect: API call
  await saveToServer({ firstName: newName })
  console.log(`Name changed from ${oldName} to ${newName}`)
})

4. Virtual DOM คืออะไร และ Vue ใช้งานอย่างไร

Virtual DOM คือการแทน DOM จริงในรูปแบบ JavaScript ที่เบา Vue เก็บโครงสร้างต้นไม้เสมือนไว้ในหน่วยความจำและคำนวณส่วนต่าง (diffing) ระหว่างสถานะเก่ากับใหม่ เพื่ออัปเดตเฉพาะสิ่งที่จำเป็นบน DOM จริง

javascript
// Conceptual representation of the Virtual DOM
// Vue creates this structure internally

const vnode = {
  type: 'div',
  props: {
    class: 'container',
    id: 'app'
  },
  children: [
    {
      type: 'h1',
      props: {},
      children: 'Title'
    },
    {
      type: 'p',
      props: {},
      children: 'Paragraph content'
    }
  ]
}

// When changes occur, Vue compares vnodes
// and updates only the modified elements

การเพิ่มประสิทธิภาพของ Vue 3 ได้แก่ การ hoist โหนดสแตติก patch flag เพื่อระบุประเภทการเปลี่ยนแปลง และ tree-shaking ในคอมไพเลอร์

5. จัดการการสื่อสารระหว่างคอมโพเนนต์ที่ไม่ได้สัมพันธ์โดยตรงอย่างไร

มีหลายรูปแบบสำหรับให้คอมโพเนนต์ที่ไม่ได้มีความสัมพันธ์ parent-child โดยตรงคุยกัน

javascript
// Solution 1: Event Bus (small applications)
// eventBus.js
import { ref } from 'vue'

const bus = ref(new Map())

export function useEventBus() {
  // Emit an event
  const emit = (event, payload) => {
    const callbacks = bus.value.get(event) || []
    callbacks.forEach(cb => cb(payload))
  }

  // Listen to an event
  const on = (event, callback) => {
    if (!bus.value.has(event)) {
      bus.value.set(event, [])
    }
    bus.value.get(event).push(callback)
  }

  return { emit, on }
}
javascript
// Solution 2: Provide/Inject for nested components
// Ancestor
import { provide, ref } from 'vue'

const sharedState = ref('shared value')
provide('stateKey', sharedState)

// Descendant (any level)
import { inject } from 'vue'

const state = inject('stateKey')

สำหรับแอปพลิเคชันที่ซับซ้อนมากขึ้น Pinia ยังเป็นทางเลือกที่แนะนำสำหรับการจัดการ state แบบ global

คำถามเกี่ยวกับ Composition API

6. Composition API มีข้อได้เปรียบอย่างไรเมื่อเทียบกับ Options API

Composition API มีจุดเด่นเชิงโครงสร้างหลายอย่างเมื่อเทียบกับ Options API ดั้งเดิม

จัดระเบียบตามฟีเจอร์: โค้ดที่เกี่ยวข้องกับฟีเจอร์เดียวกันถูกจัดวางอยู่ด้วยกัน ในขณะที่ Options API จะแยกตามประเภท (data, methods, computed)

นำกลับมาใช้ใหม่ผ่าน composables: ดึงตรรกะออกเป็นฟังก์ชันที่นำกลับมาใช้ได้ง่าย

รองรับ TypeScript ดีขึ้น: การ infer ประเภทเป็นไปอย่างเป็นธรรมชาติ ไม่ต้องใช้ decorator

javascript
// Options API: code fragmented by type
export default {
  data() {
    return {
      searchQuery: '',
      results: []
    }
  },
  computed: {
    hasResults() {
      return this.results.length > 0
    }
  },
  methods: {
    async search() {
      this.results = await fetchResults(this.searchQuery)
    }
  },
  watch: {
    searchQuery: 'search'
  }
}

// Composition API: code grouped by feature
import { ref, computed, watch } from 'vue'

export function useSearch() {
  const searchQuery = ref('')
  const results = ref([])

  const hasResults = computed(() => results.value.length > 0)

  const search = async () => {
    results.value = await fetchResults(searchQuery.value)
  }

  watch(searchQuery, search)

  return { searchQuery, results, hasResults, search }
}

7. สร้าง composable ที่นำกลับมาใช้ใหม่ได้อย่างไร

composable คือฟังก์ชันที่ห่อตรรกะ reactive ไว้ ข้อตกลงทั่วไป: ใช้คำนำหน้า use คืน object ที่มีทั้ง state และ method และจัดการ cleanup

composables/useLocalStorage.jsjavascript
import { ref, watch } from 'vue'

// Composable to synchronize state with localStorage
export function useLocalStorage(key, defaultValue) {
  // Retrieve initial value from localStorage
  const storedValue = localStorage.getItem(key)
  const data = ref(
    storedValue ? JSON.parse(storedValue) : defaultValue
  )

  // Synchronize changes to localStorage
  watch(
    data,
    (newValue) => {
      if (newValue === null) {
        localStorage.removeItem(key)
      } else {
        localStorage.setItem(key, JSON.stringify(newValue))
      }
    },
    { deep: true } // Observe nested objects
  )

  return data
}

// Usage in a component
const theme = useLocalStorage('theme', 'light')
const userPrefs = useLocalStorage('prefs', { notifications: true })
ข้อตกลงการตั้งชื่อ

composable ใช้รูปแบบ useXxx เพื่อบ่งบอกถึงคุณสมบัติที่นำกลับมาใช้ได้ ข้อตกลงนี้ทำให้โค้ดอ่านง่ายและช่วยให้สังเกต dependency แบบ reactive ได้ง่ายขึ้น

8. อธิบาย watchEffect เทียบกับ watch

watchEffect กับ watch ต่างก็ตอบสนองต่อการเปลี่ยนแปลง แต่แนวทางแตกต่างกัน

watchEffect: ทำงานทันทีและรันใหม่อัตโนมัติเมื่อ dependency แบบ reactive เปลี่ยน การติดตาม dependency เป็นไปโดยอัตโนมัติ

watch: คอยสังเกตแหล่งข้อมูลที่ระบุและให้ค่าเก่ากับค่าใหม่ มีอำนาจในการกำหนดเวลาที่จะถูกกระตุ้นมากกว่า

javascript
// watchEffect vs watch comparison
import { ref, watch, watchEffect } from 'vue'

const userId = ref(1)
const userData = ref(null)

// watchEffect: automatic tracking
// Runs immediately
watchEffect(async () => {
  // userId is automatically tracked
  const response = await fetch(`/api/users/${userId.value}`)
  userData.value = await response.json()
})

// watch: explicit sources with old values
watch(userId, async (newId, oldId) => {
  console.log(`User changed from ${oldId} to ${newId}`)
  const response = await fetch(`/api/users/${newId}`)
  userData.value = await response.json()
}, {
  immediate: true // Run immediately like watchEffect
})

// watchEffect with cleanup
watchEffect((onCleanup) => {
  const controller = new AbortController()

  fetch(`/api/users/${userId.value}`, {
    signal: controller.signal
  }).then(/* ... */)

  // Cleanup: cancel previous request
  onCleanup(() => controller.abort())
})

9. จัดการ props กับ TypeScript ใน script setup อย่างไร

ไวยากรณ์ <script setup> รองรับ TypeScript โดยตรงผ่าน defineProps และ withDefaults

TypedComponent.vuetypescript
<script setup lang="ts">
// Interface for props
interface Props {
  title: string
  count?: number
  items?: string[]
  onSubmit?: (data: FormData) => void
}

// defineProps with generic typing
// withDefaults for default values
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => [], // Factory for objects/arrays
  onSubmit: undefined
})

// Props are automatically typed
console.log(props.title) // string
console.log(props.count) // number

// defineEmits with typing
const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'delete', id: string): void
}>()

// Typed emit usage
const handleUpdate = () => {
  emit('update', props.count + 1)
}
</script>

พร้อมที่จะพิชิตการสัมภาษณ์ Vue.js / Nuxt.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

คำถามด้านประสิทธิภาพ

10. มีเทคนิคการเพิ่มประสิทธิภาพอะไรบ้าง

Vue 3 มีกลไกหลายอย่างในการเพิ่มประสิทธิภาพ

1. v-once: single render for static contentjavascript
<template>
  <div v-once>
    <!-- This content will never be re-rendered -->
    <ComplexStaticComponent />
  </div>
</template>

// 2. v-memo: conditional memoization
<template>
  <div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
    <!-- Re-renders only if id or selected changes -->
    {{ item.name }}
  </div>
</template>

// 3. shallowRef/shallowReactive: shallow reactivity
import { shallowRef, triggerRef } from 'vue'

// Only tracks ref replacement, not internal mutations
const largeList = shallowRef([/* thousands of elements */])

// Force update after mutation
largeList.value.push(newItem)
triggerRef(largeList) // Manually trigger re-render
4. Async components for code-splittingjavascript
import { defineAsyncComponent } from 'vue'

const HeavyComponent = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200, // Delay before showing loading
  errorComponent: ErrorDisplay,
  timeout: 3000
})

// 5. KeepAlive for component caching
<template>
  <KeepAlive :include="['Dashboard', 'UserProfile']" :max="10">
    <component :is="currentView" />
  </KeepAlive>
</template>

11. หลีกเลี่ยงการ re-render ที่ไม่จำเป็นได้อย่างไร

การ re-render ที่ไม่จำเป็นส่งผลต่อประสิทธิภาพ มีหลายแนวทางที่ช่วยลดให้น้อยที่สุด

javascript
// Problem: function created on each render
<template>
  <!--New function on every render -->
  <ChildComponent @click="() => handleClick(item.id)" />
</template>

// Solution: use a method or ref
<script setup>
const handleItemClick = (id) => {
  // Processing logic
}
</script>

<template>
  <!--Stable reference -->
  <ChildComponent @click="handleItemClick(item.id)" />
</template>
javascript
// Using computed for expensive calculations
import { computed } from 'vue'

// ❌ Recalculated on every render
const getFilteredItems = () => {
  return items.value.filter(/* complex logic */)
}

// ✅ Cached, recalculated only if items changes
const filteredItems = computed(() => {
  return items.value.filter(/* complex logic */)
})

12. อธิบาย lazy loading ของคอมโพเนนต์และ route

Lazy loading ทำให้โหลดโค้ดเมื่อจำเป็น ลดขนาด bundle ตอนเริ่มต้น

router/index.jsjavascript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      // Immediate loading (main bundle)
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/dashboard',
      // Separate chunk with custom name
      component: () => import(
        /* webpackChunkName: "dashboard" */
        '@/views/Dashboard.vue'
      ),
      // Lazy loading child routes
      children: [
        {
          path: 'analytics',
          component: () => import('@/views/Analytics.vue')
        }
      ]
    },
    {
      path: '/admin',
      // Prefetch on link hover
      component: () => import('@/views/Admin.vue'),
      meta: { prefetch: true }
    }
  ]
})

export default router

คำถามเกี่ยวกับ Vue Router

13. ป้องกัน route ด้วย guard อย่างไร

Navigation guard ใช้ควบคุมการเข้าถึง route

router/index.jsjavascript
import { createRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'

const router = createRouter({
  routes: [
    {
      path: '/dashboard',
      component: Dashboard,
      meta: { requiresAuth: true, roles: ['admin', 'user'] }
    }
  ]
})

// Global guard: checks authentication
router.beforeEach(async (to, from, next) => {
  const auth = useAuthStore()

  // Public route
  if (!to.meta.requiresAuth) {
    return next()
  }

  // Check authentication
  if (!auth.isAuthenticated) {
    return next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  }

  // Check roles if specified
  if (to.meta.roles && !to.meta.roles.includes(auth.user.role)) {
    return next('/unauthorized')
  }

  next()
})

// Component-level guard
export default {
  beforeRouteEnter(to, from, next) {
    // No access to this here
    next(vm => {
      // Access component instance via vm
      vm.loadData()
    })
  },
  beforeRouteLeave(to, from, next) {
    // Confirm before leaving if form modified
    if (this.hasUnsavedChanges) {
      const answer = confirm('Leave without saving?')
      next(answer)
    } else {
      next()
    }
  }
}

14. ส่ง props ให้ route อย่างไร

Vue Router ช่วยให้แยกคอมโพเนนต์ออกจากพารามิเตอร์ของ route ได้

javascript
// Route configuration with props
const routes = [
  {
    path: '/user/:id',
    component: UserProfile,
    // Boolean mode: passes params as props
    props: true
  },
  {
    path: '/search',
    component: SearchResults,
    // Function mode: custom transformation
    props: (route) => ({
      query: route.query.q,
      page: parseInt(route.query.page) || 1,
      filters: route.query.filters?.split(',') || []
    })
  },
  {
    path: '/static',
    component: StaticPage,
    // Object mode: static props
    props: { sidebar: true, theme: 'dark' }
  }
]
UserProfile.vuejavascript
<script setup>
// Props are automatically injected
defineProps<{
  id: string
}>()
</script>

// SearchResults.vue
<script setup>
defineProps<{
  query: string
  page: number
  filters: string[]
}>()
</script>

คำถามเกี่ยวกับ Pinia และการจัดการ state

15. Pinia ต่างจาก Vuex อย่างไร

Pinia คือเครื่องมือจัดการ state อย่างเป็นทางการของ Vue 3 และมาแทน Vuex ด้วย API ที่กระชับ

| คุณสมบัติ | Vuex | Pinia | |---------|------|-------| | Mutation | ต้องมี | ไม่ต้องการ | | โมดูล | ตั้งค่าซับซ้อน | store อิสระ | | TypeScript | รองรับจำกัด | รองรับเต็มในตัว | | API | Options | Composition + Options | | DevTools | รองรับ | รองรับเต็มที่ |

javascript
// Pinia Store with Composition API
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // State
  const items = ref([])
  const discountCode = ref(null)

  // Getters (computed)
  const totalItems = computed(() =>
    items.value.reduce((sum, item) => sum + item.quantity, 0)
  )

  const totalPrice = computed(() => {
    const subtotal = items.value.reduce(
      (sum, item) => sum + item.price * item.quantity, 0
    )
    return discountCode.value ? subtotal * 0.9 : subtotal
  })

  // Actions (direct functions)
  function addItem(product) {
    const existing = items.value.find(i => i.id === product.id)
    if (existing) {
      existing.quantity++
    } else {
      items.value.push({ ...product, quantity: 1 })
    }
  }

  function removeItem(productId) {
    const index = items.value.findIndex(i => i.id === productId)
    if (index > -1) {
      items.value.splice(index, 1)
    }
  }

  async function checkout() {
    const response = await api.createOrder(items.value)
    items.value = []
    return response
  }

  return {
    items, discountCode,
    totalItems, totalPrice,
    addItem, removeItem, checkout
  }
})

16. ทำให้ state ของ Pinia store คงอยู่ระหว่างเซสชันได้อย่างไร

การ persist ช่วยรักษา state ไว้ระหว่างเซสชันของผู้ใช้

plugins/piniaPersistedState.jsjavascript
import { watch } from 'vue'

export function createPersistedState(options = {}) {
  const {
    key = 'pinia',
    storage = localStorage,
    paths = null
  } = options

  return ({ store }) => {
    // Restore state on startup
    const savedState = storage.getItem(`${key}-${store.$id}`)
    if (savedState) {
      store.$patch(JSON.parse(savedState))
    }

    // Persist changes
    watch(
      () => store.$state,
      (state) => {
        const toSave = paths
          ? paths.reduce((acc, path) => {
              acc[path] = state[path]
              return acc
            }, {})
          : state

        storage.setItem(
          `${key}-${store.$id}`,
          JSON.stringify(toSave)
        )
      },
      { deep: true }
    )
  }
}

// main.js
import { createPinia } from 'pinia'
import { createPersistedState } from './plugins/piniaPersistedState'

const pinia = createPinia()
pinia.use(createPersistedState({
  key: 'app-state',
  paths: ['user', 'preferences'] // Persist only these keys
}))
ข้อมูลอ่อนไหว

ควรหลีกเลี่ยงการเก็บข้อมูลอ่อนไหว (token, รหัสผ่าน) ใน localStorage สำหรับ token การยืนยันตัวตน ควรใช้คุกกี้แบบ httpOnly จะปลอดภัยกว่า

คำถามขั้นสูง

17. สร้างระบบ plugin ใน Vue ได้อย่างไร

Plugin ใช้ขยาย Vue ด้วยฟีเจอร์ระดับ global

plugins/analyticsPlugin.jsjavascript
export const AnalyticsPlugin = {
  install(app, options = {}) {
    const { trackingId, debug = false } = options

    // Global injection available in all components
    const analytics = {
      trackEvent(category, action, label) {
        if (debug) {
          console.log('Analytics:', { category, action, label })
        }
        // Logic to send to analytics service
        window.gtag?.('event', action, {
          event_category: category,
          event_label: label
        })
      },
      trackPage(path) {
        window.gtag?.('config', trackingId, { page_path: path })
      }
    }

    // Make available via inject
    app.provide('analytics', analytics)

    // Add global property (discouraged in Composition API)
    app.config.globalProperties.$analytics = analytics

    // Custom directive for click tracking
    app.directive('track', {
      mounted(el, binding) {
        el.addEventListener('click', () => {
          analytics.trackEvent('click', binding.value, el.textContent)
        })
      }
    })

    // Automatic route change tracking
    app.mixin({
      mounted() {
        if (this.$route) {
          analytics.trackPage(this.$route.path)
        }
      }
    })
  }
}

// main.js
import { AnalyticsPlugin } from './plugins/analyticsPlugin'

app.use(AnalyticsPlugin, {
  trackingId: 'UA-XXXXX-X',
  debug: import.meta.env.DEV
})

18. อธิบาย render function และประโยชน์

Render function ให้ความควบคุมการ render อย่างเต็มที่ เหมาะกับคอมโพเนนต์ที่เปลี่ยนแปลงสูง

components/DynamicHeading.jsjavascript
import { h } from 'vue'

// Functional component with render function
export const DynamicHeading = {
  props: {
    level: {
      type: Number,
      default: 1,
      validator: (v) => v >= 1 && v <= 6
    }
  },
  setup(props, { slots }) {
    // h() creates a vnode
    // Arguments: type, props, children
    return () => h(
      `h${props.level}`,
      { class: 'dynamic-heading' },
      slots.default?.()
    )
  }
}

// Component with complex conditional logic
export const ConditionalWrapper = {
  props: ['condition', 'wrapper'],
  setup(props, { slots }) {
    return () => {
      if (props.condition) {
        return h(props.wrapper, null, slots.default?.())
      }
      return slots.default?.()
    }
  }
}

// Usage
<DynamicHeading :level="2">Level 2 Title</DynamicHeading>

<ConditionalWrapper :condition="isLink" wrapper="a">
  Conditional content
</ConditionalWrapper>

19. ทดสอบคอมโพเนนต์ Vue ด้วย Vitest อย่างไร

Unit test ใช้ตรวจสอบพฤติกรรมแบบแยกของคอมโพเนนต์

components/__tests__/Counter.spec.jsjavascript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'

describe('Counter', () => {
  it('displays the initial value', () => {
    const wrapper = mount(Counter, {
      props: { initialValue: 5 }
    })

    expect(wrapper.text()).toContain('5')
  })

  it('increments the value on click', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.text()).toContain('1')
  })

  it('emits an event when changed', async () => {
    const wrapper = mount(Counter)

    await wrapper.find('button.increment').trigger('click')

    expect(wrapper.emitted('change')).toBeTruthy()
    expect(wrapper.emitted('change')[0]).toEqual([1])
  })

  it('calls the service on submit', async () => {
    const mockSubmit = vi.fn()
    const wrapper = mount(Counter, {
      global: {
        provide: {
          submitService: mockSubmit
        }
      }
    })

    await wrapper.find('form').trigger('submit')

    expect(mockSubmit).toHaveBeenCalled()
  })
})

20. จัดการ error แบบ global ใน Vue ได้อย่างไร

Vue 3 มีกลไกหลายอย่างสำหรับดักจับและจัดการ error

main.jsjavascript
import { createApp } from 'vue'

const app = createApp(App)

// Global handler for component errors
app.config.errorHandler = (err, instance, info) => {
  // err: the error
  // instance: the component instance
  // info: string describing where the error occurred

  console.error('Vue error:', err)
  console.error('Component:', instance?.$options?.name)
  console.error('Info:', info)

  // Send to monitoring service
  errorTracker.captureException(err, {
    component: instance?.$options?.name,
    info
  })
}

// Handler for warnings (dev only)
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Vue warning:', msg)
}
javascript
// ErrorBoundary component
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

// Captures errors from child components
onErrorCaptured((err, instance, info) => {
  error.value = {
    message: err.message,
    component: instance?.$options?.name,
    info
  }

  // Return false to stop propagation
  return false
})

const retry = () => {
  error.value = null
}
</script>

<template>
  <div v-if="error" class="error-boundary">
    <h2>An error occurred</h2>
    <p>{{ error.message }}</p>
    <button @click="retry">Retry</button>
  </div>
  <slot v-else />
</template>

พร้อมที่จะพิชิตการสัมภาษณ์ Vue.js / Nuxt.js แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

คำถามเกี่ยวกับแนวทางที่ดี

21. ควรปฏิบัติตามกฎการตั้งชื่อใดใน Vue

กฎการตั้งชื่อช่วยให้โค้ดอ่านและดูแลรักษาได้ง่ายขึ้น

javascript
// Component naming
// PascalCase for files and names
// BaseButton.vue, AppHeader.vue, TheNavbar.vue

// Props: camelCase in JS, kebab-case in template
defineProps<{
  userName: string        // JS
  isActive: boolean       // JS
}>()

// <UserCard :user-name="name" :is-active="active" />

// Events: camelCase with action prefix
const emit = defineEmits<{
  (e: 'updateValue', value: string): void  // ✅
  (e: 'submit'): void                       // ✅
  (e: 'value-updated'): void               // ❌ Avoid
}>()

// Composables: use prefix
// useAuth.js, useFetch.js, useLocalStorage.js

// Pinia stores: use prefix + Store suffix
// useUserStore, useCartStore, useSettingsStore

// Constants: SCREAMING_SNAKE_CASE
const MAX_RETRY_COUNT = 3
const API_BASE_URL = '/api/v1'

22. จัดโครงสร้างโปรเจกต์ Vue ขนาดใหญ่อย่างไร

โครงสร้างแบบโมดูลช่วยให้นำทางและบำรุงรักษาง่ายขึ้น

text
src/
├── assets/              # Static files
├── components/
│   ├── ui/              # Generic components (Button, Modal)
│   └── common/          # Reusable business components
├── composables/         # Reusable logic
│   ├── useAuth.js
│   └── useFetch.js
├── layouts/             # Page layouts
│   ├── DefaultLayout.vue
│   └── AuthLayout.vue
├── modules/             # Functional modules
│   ├── auth/
│   │   ├── components/
│   │   ├── composables/
│   │   ├── stores/
│   │   └── views/
│   └── dashboard/
├── plugins/             # Vue plugins
├── router/
│   ├── index.js
│   └── guards.js
├── stores/              # Global Pinia stores
├── types/               # TypeScript types
├── utils/               # Pure utilities
└── views/               # Pages/Routes

23. เมื่อใดควรใช้ v-if แทน v-show

การเลือกระหว่าง v-if กับ v-show ขึ้นกับความถี่ของการสลับสถานะ

javascript
// v-if: low initial cost, expensive toggle
// Removes/adds element from DOM
// Ideal for: rarely modified conditions

<template>
  <!-- v-if: component not created if not admin -->
  <AdminPanel v-if="user.isAdmin" />

  <!-- v-if with v-else-if for multiple conditions -->
  <LoadingSpinner v-if="isLoading" />
  <ErrorMessage v-else-if="error" :message="error" />
  <DataDisplay v-else :data="data" />
</template>

// v-show: higher initial cost, fast toggle
// Uses display: none
// Ideal for: frequent toggles

<template>
  <!-- v-show: always rendered, frequent toggle -->
  <Tooltip v-show="isHovered">
    Contextual information
  </Tooltip>

  <!-- Accordion menu with frequent toggle -->
  <div v-show="isExpanded" class="accordion-content">
    {{ content }}
  </div>
</template>

24. ปรับแต่งลิสต์ด้วย v-for อย่างไร

การปรับแต่งลิสต์เป็นเรื่องสำคัญเมื่อมีจำนวนรายการมาก

javascript
// Always use :key with a unique stable identifier
<template>
  <!--Unique and stable key -->
  <li v-for="item in items" :key="item.id">
    {{ item.name }}
  </li>

  <!--Index as key (reordering issues) -->
  <li v-for="(item, index) in items" :key="index">
    {{ item.name }}
  </li>
</template>

// Filtering and sorting: use computed
<script setup>
import { computed } from 'vue'

const sortedAndFilteredItems = computed(() => {
  return items.value
    .filter(item => item.isActive)
    .sort((a, b) => a.name.localeCompare(b.name))
})
</script>

// Virtualization for very long lists
<script setup>
import { useVirtualList } from '@vueuse/core'

const { list, containerProps, wrapperProps } = useVirtualList(
  largeList,
  { itemHeight: 50 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 400px; overflow: auto">
    <div v-bind="wrapperProps">
      <div v-for="item in list" :key="item.data.id">
        {{ item.data.name }}
      </div>
    </div>
  </div>
</template>

25. อธิบายแพตเทิร์น Renderless component

คอมโพเนนต์แบบ renderless ห่อตรรกะไว้โดยไม่กำหนดโครงสร้าง HTML

components/MouseTracker.vuejavascript
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const x = ref(0)
const y = ref(0)

const updatePosition = (event) => {
  x.value = event.clientX
  y.value = event.clientY
}

onMounted(() => {
  window.addEventListener('mousemove', updatePosition)
})

onUnmounted(() => {
  window.removeEventListener('mousemove', updatePosition)
})

// Expose state via slot
defineExpose({ x, y })
</script>

<template>
  <!-- Slot with props: parent decides the rendering -->
  <slot :x="x" :y="y" />
</template>
javascript
// Usage: full control over rendering
<template>
  <MouseTracker v-slot="{ x, y }">
    <div class="cursor-display">
      Position: {{ x }}, {{ y }}
    </div>
  </MouseTracker>

  <MouseTracker v-slot="{ x, y }">
    <svg>
      <circle :cx="x" :cy="y" r="10" fill="red" />
    </svg>
  </MouseTracker>
</template>

แพตเทิร์นนี้แยกตรรกะออกจากการนำเสนออย่างสมบูรณ์ ทำให้นำกลับมาใช้ได้สูงสุด

บทสรุป

คำถามทั้ง 25 ข้อนี้ครอบคลุมแนวคิดสำคัญที่มักถูกประเมินในการสัมภาษณ์ Vue.js:

  • Reactivity: ref, reactive, computed, watch
  • Composition API: composable, script setup, TypeScript
  • ประสิทธิภาพ: lazy loading, virtualization, การปรับแต่ง
  • Vue Router: guard, props, การนำทาง
  • Pinia: store, persistence, action แบบอะซิงโครนัส
  • แนวทางที่ดี: โครงสร้าง ข้อตกลง แพตเทิร์นขั้นสูง

การเตรียมตัวที่มีประสิทธิภาพควรผสานความเข้าใจเชิงทฤษฎีกับการลงมือเขียนโค้ด แต่ละแนวคิดที่กล่าวถึงในที่นี้สามารถนำไปสู่คำถามต่อยอดเชิงลึกในห้องสัมภาษณ์ได้

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

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

แท็ก

#vue.js
#interview
#frontend
#javascript
#technical questions

แชร์

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