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.

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.
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.
// 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 }) // ✅ CorrectQuy 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.
// 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ộ.
// 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.
// 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 elementsCá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.
// 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 }
}// 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.
// 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.
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 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.
// 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 <script setup> tích hợp TypeScript một cách tự nhiên thông qua defineProps và withDefaults.
<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>Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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.
<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-renderimport { 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. 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.
// 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>// 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.
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 routerCâ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.
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.
// 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' }
}
]<script setup>
// Props are automatically injected
defineProps<{
id: string
}>()
</script>
// SearchResults.vue
<script setup>
defineProps<{
query: string
page: number
filters: string[]
}>()
</script>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 đủ |
// 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.
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
}))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.
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.
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. Làm sao kiểm thử component Vue với Vitest?
Unit test xác minh hành vi đơn lẻ của component.
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.
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)
}// 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>Sẵn sàng chinh phục phỏng vấn Vue.js / Nuxt.js?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
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.
// 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. 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/Routes23. 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.
// 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. 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.
// 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. 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.
<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>// 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>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.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

Vue 3 Composition API: Hướng Dẫn Đầy Đủ Để Làm Chủ Reactivity
Làm chủ Vue 3 Composition API qua hướng dẫn thực hành này. Tìm hiểu ref, reactive, computed, watch và composables để xây dựng ứng dụng Vue hiệu suất cao.

Vue 3 Pinia vs Vuex: So Sanh State Management va Cau Hoi Phong Van 2026
Phan tich chi tiet Pinia vs Vuex: kien truc, TypeScript, Composition API, hieu suat, chien luoc migration va cau hoi phong van Vue state management 2026.

Nuxt 3: SSR và sinh trang tĩnh, hướng dẫn đầy đủ
Làm chủ SSR và sinh trang tĩnh với Nuxt 3. Từ useFetch đến route rules, tìm hiểu cách tối ưu hiệu năng các ứng dụng Vue.js.