# Câu hỏi phỏng vấn Vue.js: 25 câu để chinh phục công việc > Chuẩn bị phỏng vấn Vue.js với 25 câu hỏi cốt lõi. Từ reactivity đến composables, làm chủ các khái niệm quan trọng cho buổi phỏng vấn tới. - Published: 2026-02-05 - Updated: 2026-04-27 - Author: SharpSkill - Tags: vue.js, interview, frontend, javascript, technical questions - Reading time: 15 min --- Phỏng vấn Vue.js đánh giá rất nhiều thứ vượt ra khỏi cú pháp framework. Nhà tuyển dụng muốn xác định mức độ thành thạo hệ thống reactivity, khả năng tổ chức mã với Composition API và năng lực giải quyết các vấn đề thực tế về hiệu năng cũng như kiến trúc. > **Lời khuyên chuẩn bị** > > Mỗi câu hỏi đều có lời giải chi tiết và ví dụ mã. Trong các buổi phỏng vấn kỹ thuật, việc luyện tập trình bày to khái niệm như đang phỏng vấn thật sẽ rất hữu ích. ## Câu hỏi nền tảng về Vue.js ### 1. Sự khác nhau giữa ref và reactive trong Vue 3 là gì? Câu hỏi này đánh giá hiểu biết về hệ thống reactivity, một trong những trụ cột của Vue 3. Khác biệt chính nằm ở loại dữ liệu được xử lý và cú pháp truy cập. `ref` tạo ra một tham chiếu reactive cho các giá trị nguyên thủy (string, number, boolean) và đòi hỏi `.value` để truy cập giá trị trong script. `reactive` tạo ra một proxy reactive cho object và cho phép truy cập trực tiếp đến thuộc tính. ```javascript // Ví dụ so sánh ref vs 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 ``` Quy tắc chung: dùng `ref` cho giá trị đơn giản và `reactive` cho object có nhiều thuộc tính liên quan. ### 2. Hệ thống reactivity của Vue 3 hoạt động ra sao? Vue 3 dùng Proxy của JavaScript (ES6) để chặn các thao tác trên object reactive. Khác với Vue 2 vốn dựa vào `Object.defineProperty`, cách tiếp cận này phát hiện việc thêm hoặc xóa thuộc tính một cách động. ```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) ``` Ưu điểm của Proxy: phát hiện thuộc tính mới, hỗ trợ Map và Set, hiệu năng tốt hơn trên object lớn. ### 3. Hãy giải thích sự khác nhau giữa computed và watch `computed` và `watch` phục vụ những nhu cầu khác nhau trong việc quản lý reactivity. **Computed**: tính toán giá trị suy ra từ các dữ liệu reactive khác. Giá trị được lưu cache và chỉ tính lại khi dependency thay đổi. Phù hợp cho biến đổi dữ liệu. **Watch**: thực thi side effect khi có thay đổi. Hữu ích cho gọi API, tương tác DOM hoặc các thao tác bất đồng bộ. ```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 là gì và Vue sử dụng nó thế nào? Virtual DOM là biểu diễn nhẹ trong JavaScript của DOM thực. Vue duy trì một cây ảo trong bộ nhớ và tính toán chênh lệch (diffing) giữa trạng thái cũ và mới để áp dụng đúng những thay đổi cần thiết lên DOM thực. ```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 ``` Các tối ưu của Vue 3 bao gồm: hoist node tĩnh, patch flag để xác định kiểu thay đổi và tree-shaking trong compiler. ### 5. Làm sao để xử lý giao tiếp giữa các component không có quan hệ trực tiếp? Có nhiều mẫu để cho các component không có quan hệ cha-con trực tiếp giao tiếp với nhau. ```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') ``` Với những ứng dụng phức tạp hơn, Pinia vẫn là giải pháp được khuyên dùng để quản lý state toàn cục. ## Câu hỏi về Composition API ### 6. Composition API có ưu điểm gì so với Options API? Composition API có nhiều ưu điểm cấu trúc so với Options API truyền thống. **Tổ chức theo tính năng**: mã liên quan đến cùng một tính năng được nhóm lại, trong khi Options API tách theo loại (data, methods, computed). **Tái sử dụng nhờ composables**: trích logic ra hàm dùng lại được dễ dàng. **Hỗ trợ TypeScript tốt hơn**: suy luận kiểu tự nhiên, không cần 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. Làm thế nào để tạo một composable dùng lại được? Composable là hàm bao gói logic reactive. Quy ước: tiền tố `use`, trả về object gồm state và method, đồng thời xử lý cleanup. ```javascript // composables/useLocalStorage.js 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 }) ``` > **Quy ước đặt tên** > > Composable tuân theo quy ước `useXxx` để thể hiện tính chất tái sử dụng. Quy ước này giúp mã dễ đọc và dễ nhận diện các phụ thuộc reactive. ### 8. Hãy giải thích watchEffect so với watch `watchEffect` và `watch` đều phản ứng với thay đổi nhưng theo cách khác nhau. **watchEffect**: chạy ngay lập tức và tự động chạy lại khi các phụ thuộc reactive thay đổi. Việc theo dõi phụ thuộc diễn ra tự động. **watch**: quan sát các nguồn cụ thể và cung cấp cả giá trị cũ lẫn mới. Cho phép kiểm soát thời điểm kích hoạt nhiều hơn. ```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. Làm sao để xử lý props với TypeScript trong script setup? Cú pháp ` ``` ## Câu hỏi về hiệu năng ### 10. Có những kỹ thuật tối ưu hiệu năng nào? Vue 3 cung cấp nhiều cơ chế tối ưu hiệu năng. ```javascript // 1. v-once: single render for static content // 2. v-memo: conditional memoization // 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 ``` ```javascript // 4. Async components for code-splitting 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 ``` ### 11. Làm sao tránh re-render không cần thiết? Re-render không cần thiết ảnh hưởng đến hiệu năng. Có nhiều chiến lược giúp giảm thiểu chúng. ```javascript // Problem: function created on each render // Solution: use a method or ref ``` ```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. Hãy giải thích lazy loading cho component và route Lazy loading cho phép tải mã theo nhu cầu, giảm dung lượng bundle ban đầu. ```javascript // router/index.js 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 ``` ## Câu hỏi về Vue Router ### 13. Làm sao bảo vệ route bằng guard? Navigation guard cho phép kiểm soát quyền truy cập route. ```javascript // router/index.js 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. Làm thế nào truyền props vào route? Vue Router cho phép tách rời component khỏi tham số 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' } } ] ``` ```javascript // UserProfile.vue // SearchResults.vue ``` ## Câu hỏi về Pinia và quản lý state ### 15. Pinia khác Vuex ở những điểm nào? Pinia là trình quản lý state chính thức cho Vue 3, thay thế Vuex bằng API đơn giản hơn. | Đặc điểm | Vuex | Pinia | |---------|------|-------| | Mutation | Bắt buộc | Không cần | | Module | Cấu hình phức tạp | Store độc lập | | TypeScript | Hỗ trợ giới hạn | Tích hợp đầy đủ | | API | Options | Composition + Options | | DevTools | Có hỗ trợ | Hỗ trợ đầy đủ | ```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. Làm sao để duy trì state của Pinia? Persistence giúp giữ state giữa các phiên người dùng. ```javascript // plugins/piniaPersistedState.js 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 })) ``` > **Dữ liệu nhạy cảm** > > Nên tránh lưu trữ dữ liệu nhạy cảm (token, mật khẩu) trong localStorage. Với token xác thực, cookie httpOnly sẽ an toàn hơn. ## Câu hỏi nâng cao ### 17. Làm thế nào triển khai hệ thống plugin trong Vue? Plugin cho phép mở rộng Vue bằng các tính năng toàn cục. ```javascript // plugins/analyticsPlugin.js 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. Hãy giải thích render function và công dụng của chúng Render function cho phép kiểm soát hoàn toàn quá trình render, hữu ích với những component rất linh động. ```javascript // components/DynamicHeading.js 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 Level 2 Title Conditional content ``` ### 19. Làm sao kiểm thử component Vue với Vitest? Unit test xác minh hành vi đơn lẻ của component. ```javascript // components/__tests__/Counter.spec.js 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. Làm sao xử lý lỗi toàn cục trong Vue? Vue 3 cung cấp nhiều cơ chế để bắt và xử lý lỗi. ```javascript // main.js 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 ``` ## Câu hỏi về thực hành tốt ### 21. Cần tuân thủ quy ước đặt tên nào trong Vue? Quy ước đặt tên giúp mã dễ đọc và dễ bảo trì hơn. ```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 }>() // // 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. Làm sao tổ chức một dự án Vue lớn? Cấu trúc theo module giúp việc duyệt và bảo trì dễ dàng. ``` 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. Khi nào dùng v-if thay vì v-show? Lựa chọn giữa `v-if` và `v-show` phụ thuộc vào tần suất bật/tắt. ```javascript // v-if: low initial cost, expensive toggle // Removes/adds element from DOM // Ideal for: rarely modified conditions // v-show: higher initial cost, fast toggle // Uses display: none // Ideal for: frequent toggles ``` ### 24. Làm sao tối ưu danh sách với v-for? Tối ưu danh sách rất quan trọng khi số lượng phần tử lớn. ```javascript // Always use :key with a unique stable identifier // Filtering and sorting: use computed // Virtualization for very long lists ``` ### 25. Hãy giải thích pattern Renderless component Component renderless bao gói logic mà không áp đặt cấu trúc HTML. ```javascript // components/MouseTracker.vue ``` ```javascript // Usage: full control over rendering ``` Pattern này tách hoàn toàn logic khỏi giao diện trình bày, tối đa hóa khả năng tái sử dụng. ## Kết luận 25 câu hỏi này bao quát các khái niệm cốt lõi được đánh giá trong phỏng vấn Vue.js: - ✅ **Reactivity**: `ref`, `reactive`, `computed`, `watch` - ✅ **Composition API**: composable, `script setup`, TypeScript - ✅ **Hiệu năng**: lazy loading, virtual list, các tối ưu - ✅ **Vue Router**: guard, props, điều hướng - ✅ **Pinia**: store, persistence, action bất đồng bộ - ✅ **Thực hành tốt**: cấu trúc, quy ước, pattern nâng cao Việc chuẩn bị hiệu quả phải kết hợp giữa hiểu lý thuyết và thực hành mã. Mỗi khái niệm trên đều có thể trở thành điểm khởi đầu cho những câu hỏi sâu hơn trong buổi phỏng vấn. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/vue-nuxt/essential-vuejs-interview-questions