Luyện Phỏng Vấn IT — 2000+ Câu Hỏi Phỏng Vấn IT Có Đáp Án 2026

Vue.js là progressive JavaScript framework để xây dựng UI.

  1. So với React: Vue có template syntax rõ ràng hơn, two-way binding tích hợp sẵn, learning curve thấp hơn; React dùng JSX, unidirectional flow, hệ sinh thái lớn hơn
  2. So với Angular: Vue nhẹ hơn, ít opinionated hơn; Angular là full framework với DI, OOP, TypeScript bắt buộc
  3. Progressive: có thể dùng để add vào một phần app hoặc build full SPA

Options API (Vue 2 style): tổ chức code theo loại (data, methods, computed, watch).

Composition API (Vue 3): tổ chức code theo logic concern, dùng setup() hoặc <script setup>.

  1. Composition API dễ tái sử dụng logic hơn qua composables
  2. TypeScript support tốt hơn trong Composition API
  3. Options API vẫn được hỗ trợ đầy đủ trong Vue 3

Khuyến nghị dùng Composition API với <script setup> cho project mới.

<script setup> là syntactic sugar cho Composition API — tất cả code bên trong tự động expose ra template mà không cần return.

  1. Gọn hơn: không cần export default, không cần return
  2. Hiệu năng tốt hơn: compiler tối ưu hóa tốt hơn
  3. TypeScript integration tốt hơn
  4. defineProps, defineEmits, defineExpose thay cho options props/emits

Pitfall: biến và function trong <script setup> khác file KHÔNG cần export — tự exposed.

Vue directives là các HTML attribute đặc biệt gắn kết reactive behavior vào DOM.

  • Phổ biến nhất: v-if/v-else/v-else-if: điều kiện render, xóa/tạo DOM thật. v-show: ẩn bằng display:none, DOM vẫn tồn tại. v-for: loop render, luôn cần :key. v-model: two-way binding cho input. v-bind (:attr): bind attribute động. v-on (@event): gắn event listener.

Pitfall: v-if và v-for không nên dùng trên cùng element — v-if ưu tiên cao hơn v-for trong Vue 3 (ngược với Vue 2), dùng <template v-for> bọc bên ngoài.

v-if: xóa hoàn toàn DOM khi false — chi phí cao khi toggle thường xuyên, nhưng không render child khi không cần (lazy). v-show: chỉ toggle display:none — DOM luôn được render, chi phí thấp khi toggle.

  • Dùng v-show khi element cần toggle thường xuyên.
  • Dùng v-if khi điều kiện ít thay đổi, hoặc khi child component có side-effects cần tránh khởi tạo.

Vue components giao tiếp qua nhiều cơ chế tùy quan hệ và chiều dữ liệu.

  1. Props (parent → child): dữ liệu đi xuống, one-way
  2. Emits (child → parent): event đi lên, gọi emit()
  3. v-model: two-way binding, kết hợp props + emits
  4. provide/inject: ancestor → descendant, bỏ qua intermediaries
  5. Pinia (recommended) hoặc Vuex (legacy): global state store cho app-wide state
  6. Event bus (ít dùng trong Vue 3): mitt library

Pitfall: tránh emit từ child để trực tiếp modify prop của parent — luôn emit event để parent tự update.

Vue Router là official router cho Vue.js — quản lý navigation, URL history, route matching.

javascript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/user/:id', component: User },
    { path: '/:pathMatch(.*)*', component: NotFound },
  ]
})

Navigate: <RouterLink to="/about"> trong template, hoặc router.push('/about') trong script. <RouterView /> là nơi component được render.

Pinia là official state management library cho Vue 3 — thay thế Vuex.

Ưu điểm:

  1. Composition API-friendly, không cần mutations
  2. TypeScript support tốt hơn nhiều
  3. Không có nested modules, mỗi store là một module độc lập
  4. Bundle nhỏ hơn (~1KB)
  5. Devtools support, hot-reload
  6. Server-side rendering support. Vuex 4 (dành cho Vue 3) hiện ở maintenance-only mode; Vuex 5 đã bị hủy — Pinia là official successor

SFC (.vue file) gom template, script, style vào một file duy nhất — tiện lợi cho development.

Cấu trúc: <template> (HTML), <script setup> (logic), <style scoped> (CSS).

  1. scoped attribute: CSS chỉ áp dụng cho component đó, tránh conflict
  2. Compiler (Vite/webpack) parse và compile SFC thành JavaScript
  3. Hỗ trợ lang attribute: <script lang="ts">, <style lang="scss">

Pitfall: scoped CSS không ảnh hưởng lên child components — dùng :deep() nếu cần.

Vue dùng :key để identify mỗi vnode khi diff — giúp tái sử dụng và reorder DOM nodes đúng cách thay vì re-render toàn bộ.

  1. Thiếu key: Vue dùng "in-place patch" — có thể gây lỗi với stateful components hoặc animation
  2. Dùng index làm key: không nên khi list có thể bị sort/filter — index thay đổi gây re-render sai
  3. Dùng unique stable ID (e.g., item.id) là best practice

v-model là shorthand: :modelValue + @update:modelValue.

Mặc định cho input:

vue
<!-- Tương đương -->
<input v-model="name" />
<input :value="name" @input="name = $event.target.value" />

Custom component v-model:

vue
<script setup>
// Child component
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
// Template: :value="modelValue" @input="emit('update:modelValue', $event)"
</script>

Vue 3 hỗ trợ multiple v-model: v-model:title, v-model:content với props tương ứng.

Vue 3 dùng ES6 Proxy để intercept get/set operations trên reactive objects.

Khi đọc property trong effect (computed, watcher, render): dependency được track.

Khi set property: trigger cập nhật tất cả dependents.

Cải tiến so với Vue 2 (dùng Object.defineProperty):

  1. Detect thêm/xóa property động
  2. Detect array index changes và .length
  3. Lazy — không cần walk toàn bộ object tree upfront

ref: wrap bất kỳ giá trị nào (primitives, objects) thành reactive container — truy cập qua .value trong script, tự unwrap trong template.
reactive: wrap object/array thành reactive Proxy — truy cập trực tiếp, không cần .value.

Khuyến nghị hiện tại (Vue 3.2+): Dùng ref cho tất cả — kể cả objects. Vue core team đã cập nhật docs ưu tiên ref universally vì reactive có gotchas: mất reactivity khi destructure, mất reactivity khi re-assign cả object. reactive chỉ nên dùng khi cần nhóm nhiều state liên quan.
Pitfall: destructure từ reactive sẽ mất reactivity — dùng toRefs():

javascript
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // giữ reactivity

computed: derive giá trị từ reactive state, kết quả được cache, chỉ recompute khi dependency thay đổi — dùng để transform/calculate data cho template.

watch: observe cụ thể một hoặc vài sources, có access vào old/new value, lazy by default — dùng khi cần side effect (API call, DOM manipulation) khi data thay đổi.

watchEffect: auto-track tất cả reactive dependencies dùng bên trong, chạy ngay khi mount — dùng khi không cần old value và muốn auto-dependency detection.

Composable là function dùng Composition API để đóng gói và tái sử dụng stateful logic.

Ví dụ:

javascript
// useFetch.js
export function useFetch(url) {
  const data = ref(null)
  const loading = ref(true)
  fetch(url).then(r => r.json()).then(d => { data.value = d; loading.value = false })
  return { data, loading }
}

So với Mixins:

  1. Không có naming collision — return value rõ ràng
  2. Source rõ ràng — biết data từ đâu
  3. Có thể nhận arguments (dynamic)
  4. Không có implicit state sharing

Mixins vẫn được hỗ trợ trong Vue 3 nhưng bị discouraged — Composition API là alternative được khuyến nghị.

Trong <script setup>, lifecycle hooks được import và dùng như functions:

javascript
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue'

onMounted(() => { console.log('mounted') })
onUnmounted(() => { /* cleanup */ })

Mapping từ Options API (bao gồm Vue 2 → Vue 3): beforeCreate/created → code trong setup() chạy thay thế, mountedonMounted, updatedonUpdated, beforeUpdateonBeforeUpdate, unmountedonUnmounted, beforeMountonBeforeMount.

  • Vue 2: beforeDestroyonBeforeUnmount, destroyedonUnmounted.

Pitfall: onMounted trong SSR (Nuxt) không chạy server-side — dùng cho browser-only code.

provide / inject cho phép truyền data qua component tree mà không cần props drilling.

Parent provide, bất kỳ descendant nào có thể inject:

javascript
// Parent
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)

// Child (bất kỳ cấp)
import { inject } from 'vue'
const theme = inject('theme', 'light') // 'light' là default

Dùng khi:

  1. Shared state cho subtree (theme, locale, auth)
  2. Plugin/library cung cấp context

Pitfall: khó debug hơn props vì data flow không explicit — dùng Symbol key để tránh naming collision.

defineProps khai báo props mà component nhận, defineEmits khai báo events mà component emit:

javascript
// TypeScript style (recommended)
const props = defineProps<{
  title: string
  count?: number
}>()

const emit = defineEmits<{
  (e: 'update', value: number): void
  (e: 'close'): void
}>()

// Dùng
emit('update', 42)

Pitfall: defineProps không thể destructure trực tiếp mà giữ reactivity trong Vue 3.4 trở về trước — dùng toRefs(props).

Từ Vue 3.5+: destructure props với const { title } = defineProps() giữ reactivity.

Slots cho phép parent inject content vào template của child component.

  • Default slot: <slot />.
  • Named slots: <slot name="header" /> — parent dùng <template #header>.
  • Scoped slot: child truyền data lên parent qua slot:
vue
<!-- Child -->
<slot :item="item" :index="i" />

<!-- Parent -->
<template #default="{ item, index }">
  <span>{{ index }}: {{ item.name }}</span>
</template>

Dùng scoped slots khi child biết cách lấy data nhưng parent quyết định cách render (render prop pattern).

Pitfall: không mix slot và v-if trên cùng <template> — tách riêng.

Trong <script setup>, component instance mặc định không expose properties ra ngoài (không access được qua template ref). defineExpose cho phép explicitly expose các methods/properties để parent gọi qua ref.

Ví dụ: defineExpose({ focus, reset }) — parent gọi childRef.value.focus(). Dùng khi cần imperative control (focus, scroll, reset form). Pitfall: không expose quá nhiều — tránh biến component thành "god object", ưu tiên event-based communication.

Navigation guards cho phép control navigation — xác thực, redirect, cancel.

javascript
// Global guard
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    return { name: 'Login' }  // redirect
  }
})

// Per-route guard
{ path: '/admin', component: Admin, beforeEnter: (to, from) => { ... } }

In-component guards (Options API): beforeRouteEnter, beforeRouteUpdate, beforeRouteLeave.

  • Trong Composition API dùng onBeforeRouteLeave, onBeforeRouteUpdate.
  • Return false để cancel, return route location để redirect, return undefined/true để proceed.

Dynamic segment: path: '/user/:id' — đọc qua route.params.id.

Lazy loading với dynamic import:

javascript
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  },
  {
    path: '/user/:id',
    // Với Vite: không cần webpackChunkName; tên chunk cấu hình qua build.rollupOptions
    component: () => import('./views/User.vue')
  }
]
  • Lazy loading: component chỉ được download khi navigate đến route đó — giảm initial bundle size.
  • Lưu ý: / webpackChunkName / là Webpack magic comment, không có tác dụng trong Vite.

Pitfall: đọc route.params.id trong <script setup>: dùng const route = useRoute().

Pinia stores được tạo với defineStore() dùng setup-store syntax (khuyến nghị), trả về refs cho state, computed getters, và action functions.

javascript
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const double = computed(() => count.value * 2)

  function increment() { count.value++ }
  function reset() { count.value = 0 }

  return { count, double, increment, reset }
})
javascript
// Trong component
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
store.increment()
console.log(store.count, store.double)

Pitfall: destructure store mất reactivity — dùng storeToRefs: const { count, double } = storeToRefs(store) (methods thì destructure thường).

Pinia store là reactive object — destructuring trực tiếp sẽ mất reactivity (tương tự reactive()). storeToRefs() convert state và getters thành refs để destructure an toàn: const { count, name } = storeToRefs(store).

  • Methods (actions) không cần storeToRefs — destructure thường: const { increment } = store.

Pitfall: nếu dùng storeToRefs với methods, chúng trở thành refs — không gọi được như function.

v-memo skip re-render một subtree nếu array dependency không thay đổi — tương tự React.memo nhưng ở template level:

vue
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.selected]">
  <!-- Chỉ re-render khi id hoặc selected thay đổi -->
  <ExpensiveComponent :item="item" />
</div>

Dùng cho:

  1. Long lists với expensive child renders
  2. Chỉ một vài properties ảnh hưởng đến render

Pitfall: không lạm dụng — dependency array phải đầy đủ, thiếu dependency sẽ gây stale render.

<KeepAlive> cache component instance khi unmount — state được giữ nguyên khi switch lại:

vue
<KeepAlive :include="['FormStep1', 'FormStep2']" :max="5">
  <component :is="currentTab" />
</KeepAlive>
  • Lifecycle hooks cho cached components: onActivated (khi cache hit, component show lại), onDeactivated (khi bị hide, không unmount).
  • Dùng cho: tab views, multi-step forms, expensive components cần giữ state.

Pitfall: cached components vẫn chiếm bộ nhớ — dùng :max để limit, tránh cache tất cả.

Dùng dynamic import để lazy load component — chỉ download khi cần:

javascript
import { defineAsyncComponent } from 'vue'

// Basic
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// Với loading/error states
const AsyncComp = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,      // delay trước khi show loading
  timeout: 3000,   // timeout sau 3s
})

Kết hợp với <Suspense> để handle loading state elegantly.

Vue cung cấp error boundary mechanism qua errorCaptured (Options API) / onErrorCaptured (Composition API):

javascript
// App.vue — global error boundary
import { onErrorCaptured } from 'vue'

onErrorCaptured((err, instance, info) => {
  console.error('Caught:', err, info)
  // Return false để prevent propagation
  return false
})
  • Global handler: app.config.errorHandler = (err, instance, info) => { ... }.
  • Error types caught: lifecycle hooks, event handlers, async errors trong setup, child component errors.

Pitfall: không catch errors trong async callbacks không thuộc Vue (setTimeout, fetch handlers) — cần try/catch thủ công.

Vue components được test bằng cách mount chúng với @vue/test-utils và assert trên rendered output và emitted events, dùng Vitest làm test runner.

javascript
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'

describe('Counter', () => {
  it('increments when button clicked', async () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 0 }
    })

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

    expect(wrapper.text()).toContain('1')
    expect(wrapper.emitted('update')).toBeTruthy()
  })

  it('renders slot content', () => {
    const wrapper = mount(Counter, {
      slots: { default: '<span>Label</span>' }
    })
    expect(wrapper.find('span').exists()).toBe(true)
  })
})

Dùng shallowMount để stub child components.

Test behavior, không test implementation details.

Vue batch DOM updates — không update ngay khi state thay đổi, mà queue updates và flush async. nextTick cho phép chờ DOM được update xong:

javascript
import { nextTick, ref } from 'vue'

const count = ref(0)

async function handleClick() {
  count.value++
  // DOM chưa update ở đây
  await nextTick()
  // DOM đã update — giờ có thể đọc DOM hoặc scroll
  console.log(document.querySelector('.count')?.textContent)
}

Dùng khi:

  1. Cần đọc DOM sau khi state update
  2. Focus element sau khi v-if toggle
  3. Scroll sau khi append item vào list

toRef: tạo ref từ một property của reactive object — giữ reactive connection:

javascript
const state = reactive({ count: 0, name: 'Vue' })
const countRef = toRef(state, 'count') // linked to state.count

toRefs: convert toàn bộ reactive object thành object of refs — dùng khi destructure:

javascript
const { count, name } = toRefs(state)

toValue (Vue 3.3+): unwrap ref hoặc getter — dùng trong composables để accept cả ref và plain value:

javascript
function useFeature(id: MaybeRefOrGetter<string>) {
  const resolvedId = toValue(id) // unwrap nếu là ref
}

Vue a11y best practices theo chuẩn ARIA và semantic HTML, kết hợp Vue-specific tooling như vue-axeeslint-plugin-vuejs-accessibility để phát hiện vấn đề sớm.

  1. Dùng semantic HTML trong templates (<button> không phải <div @click>)
  2. :aria-label, :aria-expanded, :aria-live với dynamic content
  3. Focus management với nextTickel.focus() sau modal open/close
  4. v-bind="$attrs" để inherit aria attrs
  5. axe-core hoặc vue-axe plugin để audit a11y trong dev
  6. Keyboard navigation — test với Tab, Enter, Esc
  7. Color contrast ratio ≥ 4.5:1 cho text

Dùng eslint-plugin-vuejs-accessibility để lint a11y issues.

Template ref cho phép truy cập DOM element hoặc component instance trực tiếp:

vue
<script setup>
import { ref, onMounted } from 'vue'

// Ref cho DOM element
const inputEl = ref<HTMLInputElement | null>(null)

// Ref cho component instance
const modalRef = ref<InstanceType<typeof Modal> | null>(null)

onMounted(() => {
  inputEl.value?.focus()  // Auto-focus khi mount
})

function openModal() {
  modalRef.value?.open()  // Gọi method của child component
}
</script>

<template>
  <input ref="inputEl" type="text" />
  <Modal ref="modalRef" />
</template>

Pitfall: ref.valuenull trước khi component mount — luôn kiểm tra trong onMounted hoặc dùng optional chaining ref.value?.method().

Pattern chuẩn để fetch async data trong Vue 3 Composition API là dùng onMounted với try/catch/finally, hoặc trích xuất thành composable tái sử dụng.

vue
<script setup>
import { ref, onMounted } from 'vue'

const users = ref([])
const loading = ref(true)
const error = ref(null)

// Pattern 1: onMounted (phổ biến nhất)
onMounted(async () => {
  try {
    const res = await fetch('/api/users')
    users.value = await res.json()
  } catch (e) {
    error.value = e.message
  } finally {
    loading.value = false
  }
})

// Pattern 2: Composable reusable
// useFetch.ts — tái sử dụng ở nhiều components
</script>

<template>
  <div v-if="loading">Loading...</div>
  <div v-else-if="error">Error: {{ error }}</div>
  <ul v-else>
    <li v-for="u in users" :key="u.id">{{ u.name }}</li>
  </ul>
</template>

Pitfall: không gọi async setup() trực tiếp mà không có <Suspense> bọc ngoài — dùng onMounted hoặc composable thay.

Với Nuxt: dùng useFetch() / useAsyncData() để SSR-compatible.

Vue tự động unwrap refs trong một số contexts — hiểu rõ để tránh bugs:

Template auto-unwrap: refs được auto-unwrap trong template, không cần .value:

vue
<script setup>
const count = ref(0)
const state = reactive({ count: ref(0) })
</script>

<template>
  <!-- Tự động unwrap — không cần .value -->
  <p>{{ count }}</p>
  <p>{{ state.count }}</p>
</template>

Reactive object unwrap: ref là property của reactive object được tự động unwrap khi access:

typescript
const count = ref(0)
const state = reactive({ count })  // Wrap ref trong reactive

console.log(state.count)  // 0, không phải Ref<0> — auto-unwrap!
state.count++             // Tương đương count.value++
console.log(count.value)  // 1 — linked!

Không unwrap: ref trong array hoặc Map KHÔNG tự động unwrap:

typescript
const list = reactive([ref(0)])
console.log(list[0].value)  // Phải dùng .value!

const map = reactive(new Map([['key', ref(0)]]))
console.log(map.get('key').value)  // Phải dùng .value!

Route meta cho phép đính kèm custom data vào routes — thường dùng cho auth, breadcrumbs, page titles:

typescript
// router/index.ts — declare meta type
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    roles?: string[]
    title?: string
    breadcrumb?: string
  }
}

const routes = [
  {
    path: '/admin',
    component: AdminLayout,
    meta: { requiresAuth: true, roles: ['admin'], title: 'Admin Panel' },
    children: [
      {
        path: 'users',
        component: UsersView,
        meta: { breadcrumb: 'Users Management' },
      }
    ]
  },
]

// Global guard dùng meta
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// Component access
const route = useRoute()
console.log(route.meta.title)  // Type-safe nhờ declaration merging

Pitfall: to.meta tổng hợp metadata từ tất cả matched routes (parent + child) — child ghi đè parent.

Vue Router cho phép kiểm soát scroll behavior khi navigate:

typescript
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // savedPosition: vị trí scroll trước đó khi dùng browser back/forward
    if (savedPosition) {
      return savedPosition  // Restore khi back/forward
    }

    // Scroll đến named anchor
    if (to.hash) {
      return {
        el: to.hash,
        behavior: 'smooth',
        top: 80,  // Offset cho fixed header
      }
    }

    // Scroll về đầu trang khi navigate thường
    return { top: 0, behavior: 'smooth' }
  },
})

// Async scroll — chờ transition xong
scrollBehavior(to, from, savedPosition) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ top: 0, behavior: 'smooth' })
    }, 300)  // Chờ page transition 300ms
  })
}

Dùng savedPosition để implement proper back/forward scroll restoration — trải nghiệm native-like.

Dùng InjectionKey để provide/inject type-safe, tránh string key collisions:

typescript
// keys.ts — export typed keys
import type { InjectionKey, Ref } from 'vue'

export interface UserContext {
  id: number
  email: string
  role: string
}

export const userContextKey: InjectionKey<UserContext> = Symbol('userContext')
export const themeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')

// Parent component
import { provide, ref } from 'vue'
import { userContextKey, themeKey } from './keys'

const theme = ref<'light' | 'dark'>('dark')
provide(themeKey, theme)
provide(userContextKey, { id: 1, email: 'user@test.com', role: 'admin' })

// Child component — fully typed
import { inject } from 'vue'
import { themeKey, userContextKey } from './keys'

const theme = inject(themeKey)     // Ref<'light' | 'dark'> | undefined
const user = inject(userContextKey)  // UserContext | undefined

// Với default value — loại bỏ undefined
const theme = inject(themeKey, ref('light'))  // Ref<'light' | 'dark'>

Dùng Symbol làm key thay vì string để tránh naming collision trong large apps hoặc libraries.

v-once: render element/component một lần duy nhất — skip future updates.

Sau khi render, element được xử lý như static content:

vue
<!-- Static content không bao giờ thay đổi -->
<h1 v-once>{{ appName }}</h1>

<!-- Expensive initial render, không bao giờ update -->
<StaticBanner v-once :data="bannerConfig" />

<!-- Kết hợp với v-for — freeze sau render đầu -->
<div v-for="item in list" :key="item.id" v-once>
  {{ item.name }}
</div>

v-pre: skip compilation toàn bộ subtree — dùng cho content muốn hiển thị mustache syntax như raw text:

vue
<!-- Hiển thị '{{ this is NOT compiled }}' -->
<span v-pre>{{ this is NOT compiled }}</span>

<!-- Dùng trong docs/code display components -->
<code v-pre>const x = ref(0)</code>

Khác nhau: v-once render rồi freeze (tốt cho performance). v-pre skip compile entirely (tốt cho mustache syntax display).

Pitfall: v-once trong component vẫn chạy setup() và reactive setup — chỉ DOM update bị skip.

Built-in modifiers:
- .lazy: sync sau change event thay vì input — giảm updates
- .number: auto convert sang number (tương đương parseFloat)
- .trim: auto trim whitespace

vue
<input v-model.lazy="search" />       <!-- Update khi blur/enter -->
<input v-model.number="age" type="number" />
<input v-model.trim="name" />

Custom modifiers cho component v-model:

typescript
// Child component
const props = defineProps<{
  modelValue: string
  modelModifiers?: { uppercase?: boolean; capitalize?: boolean }
}>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

function handleInput(e: Event) {
  let value = (e.target as HTMLInputElement).value
  if (props.modelModifiers?.uppercase) value = value.toUpperCase()
  if (props.modelModifiers?.capitalize) value = value.charAt(0).toUpperCase() + value.slice(1)
  emit('update:modelValue', value)
}
vue
<!-- Parent usage -->
<MyInput v-model.uppercase="text" />
<MyInput v-model.capitalize="text" />

<!-- Multiple v-model với modifiers -->
<MyInput v-model:title.capitalize="title" v-model:content="content" />