Vue 3 Reactivity Transform trong 2026: $ref, $computed và Câu hỏi Phỏng vấn

Hướng dẫn chuyên sâu về Reactivity Transform trong Vue 3 bao gồm $ref, $computed, và các câu hỏi phỏng vấn thường gặp cho lập trình viên Vue cấp cao.

Vue 3 Reactivity Transform $ref và $computed

Reactivity Transform là tính năng thử nghiệm trong Vue 3 nhằm đơn giản hóa việc sử dụng reactive state bằng cách loại bỏ nhu cầu truy cập .value trên ref. Mặc dù tính năng này đã bị loại bỏ khỏi Vue core từ phiên bản 3.4, việc hiểu về khái niệm và các giải pháp thay thế vẫn có giá trị trong các cuộc phỏng vấn Vue developer vì thể hiện kiến thức sâu về hệ thống reactivity của Vue.

Trạng thái Reactivity Transform

Reactivity Transform ($ref, $computed) đã bị loại bỏ từ Vue 3.4 trở lên. Vue Macros cung cấp implementation từ cộng đồng cho các team vẫn cần tính năng này. Hiểu về khái niệm này vẫn có giá trị cho phỏng vấn vì thể hiện kiến thức về sự phát triển của Vue.

Hiểu vấn đề mà Reactivity Transform giải quyết

Hệ thống reactivity của Vue 3 dựa trên các đối tượng refreactive. Khi sử dụng ref, developer phải truy cập giá trị thông qua thuộc tính .value trong script, trong khi ở template việc truy cập trực tiếp không cần .value được cho phép nhờ unwrapping tự động.

typescript
import { ref, computed } from 'vue'

// Cách tiếp cận chuẩn với .value
const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
  console.log(doubled.value)
}

Reactivity Transform loại bỏ boilerplate .value bằng các macro compiler biến đổi code tại thời điểm build.

typescript
// Với Reactivity Transform (deprecated)
let count = $ref(0)
const doubled = $computed(() => count * 2)

function increment() {
  count++
  console.log(doubled)
}

Cách hoạt động của $ref và $computed

Macro $ref khai báo biến reactive có thể được sử dụng như biến JavaScript thông thường. Compiler của Vue biến đổi code này thành việc sử dụng ref chuẩn với .value phù hợp.

typescript
// Source code với $ref
let username = $ref('')
let isValid = $computed(() => username.length >= 3)

function updateUsername(value: string) {
  username = value
}

// Kết quả sau khi compiler transform
import { ref, computed } from 'vue'

const username = ref('')
const isValid = computed(() => username.value.length >= 3)

function updateUsername(value: string) {
  username.value = value
}

Macro $$ để Escape Ref

Macro $$ cho phép truy cập đối tượng ref bên dưới khi cần thiết, ví dụ khi truyền ref vào composable hoặc watch.

typescript
import { watch } from 'vue'

let count = $ref(0)

// Sử dụng $$ để lấy ref object
watch($$(count), (newVal, oldVal) => {
  console.log(`Count thay đổi từ ${oldVal} sang ${newVal}`)
})

// Truyền ref vào composable
function useDoubled(countRef: Ref<number>) {
  return computed(() => countRef.value * 2)
}

const doubled = useDoubled($$(count))

Reactive Props Destructure

Vue 3.5 giới thiệu destructuring props reactive như một tính năng ổn định, thay thế một phần chức năng của Reactivity Transform cho props.

vue
<script setup lang="ts">
// Vue 3.5+ reactive props destructure
const { count = 0, title } = defineProps<{
  count?: number
  title: string
}>()

// count và title vẫn reactive
const doubled = computed(() => count * 2)
</script>

<template>
  <h1>{{ title }}</h1>
  <p>Count: {{ count }}, Doubled: {{ doubled }}</p>
</template>

Trước Vue 3.5, destructuring props làm mất reactivity và cần toRefs:

Trước Vue 3.5typescript
const props = defineProps<{ count: number }>()
const { count } = toRefs(props) // Bắt buộc để giữ reactivity

Implementation thay thế với Vue Macros

Vue Macros là bộ sưu tập macro được duy trì bởi cộng đồng, cung cấp chức năng Reactivity Transform và các tính năng bổ sung khác.

vite.config.tstypescript
import Vue from '@vitejs/plugin-vue'
import VueMacros from 'unplugin-vue-macros/vite'

export default defineConfig({
  plugins: [
    VueMacros({
      plugins: {
        vue: Vue(),
      },
    }),
  ],
})
vue
<script setup lang="ts">
// Với Vue Macros
let count = $ref(0)
let items = $ref<string[]>([])

const total = $computed(() => items.length)
const isEmpty = $computed(() => total === 0)

function addItem(item: string) {
  items = [...items, item]
  count++
}
</script>

Pattern hiện đại không cần Reactivity Transform

Cách tiếp cận được khuyến nghị trong Vue 3.5 sử dụng các tính năng built-in mà không cần macro bên ngoài.

vue
<script setup lang="ts">
import { ref, computed, watch } from 'vue'

// Quy ước đặt tên: sử dụng suffix Ref để rõ ràng
const countRef = ref(0)
const itemsRef = ref<string[]>([])

// Computed properties
const total = computed(() => itemsRef.value.length)
const isEmpty = computed(() => total.value === 0)

// Composable với reactive state
function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue
  
  return {
    count,
    increment,
    decrement,
    reset
  }
}

const { count, increment, decrement } = useCounter(10)
</script>

So sánh ref vs reactive

Việc hiểu khi nào sử dụng ref so với reactive thường xuất hiện trong phỏng vấn Vue.

typescript
import { ref, reactive, toRefs } from 'vue'

// ref: cho primitive values và single values
const count = ref(0)
const name = ref('Vue')
const isLoading = ref(false)

// reactive: cho objects với nhiều properties
const state = reactive({
  user: null as User | null,
  posts: [] as Post[],
  pagination: {
    page: 1,
    perPage: 10
  }
})

// Destructure reactive object với toRefs
const { user, posts } = toRefs(state)

// Ưu điểm của ref:
// 1. Có thể reassign hoàn toàn
// 2. Có thể truyền vào functions mà không mất reactivity
// 3. Predictable hơn với TypeScript

// Ưu điểm của reactive:
// 1. Không cần .value cho nested access
// 2. Tự nhiên hơn cho complex state objects
// 3. Automatic unwrapping trong template

Advanced Reactivity Patterns

Các pattern nâng cao thường được hỏi trong phỏng vấn Vue developer cấp cao.

Custom Ref với Validation

typescript
import { customRef } from 'vue'

function useDebouncedRef<T>(value: T, delay = 300) {
  let timeout: ReturnType<typeof setTimeout>
  
  return customRef<T>((track, trigger) => ({
    get() {
      track()
      return value
    },
    set(newValue: T) {
      clearTimeout(timeout)
      timeout = setTimeout(() => {
        value = newValue
        trigger()
      }, delay)
    }
  }))
}

// Sử dụng
const searchQuery = useDebouncedRef('', 500)

Shallow Reactivity cho Performance

typescript
import { shallowRef, shallowReactive, triggerRef } from 'vue'

// shallowRef: chỉ level đầu tiên là reactive
const largeList = shallowRef<Item[]>([])

function updateList(items: Item[]) {
  largeList.value = items // Triggers update
}

function mutateItem(index: number) {
  largeList.value[index].name = 'Updated'
  triggerRef(largeList) // Cần trigger thủ công
}

// shallowReactive: chỉ properties level đầu tiên là reactive
const config = shallowReactive({
  theme: 'dark',
  settings: { /* nested không reactive */ }
})

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 Phỏng vấn Vue Reactivity

Câu hỏi 1: Giải thích sự khác biệt giữa ref và reactive

Câu trả lời mong đợi: ref bọc bất kỳ giá trị nào (primitive hoặc object) trong một đối tượng có thuộc tính .value, trong khi reactive tạo proxy reactive từ object. ref linh hoạt hơn vì có thể chứa primitive và có thể reassign hoàn toàn. reactive ergonomic hơn cho complex nested objects nhưng không thể reassign và mất reactivity nếu destructure mà không có toRefs.

Câu hỏi 2: Tại sao Reactivity Transform bị loại bỏ khỏi Vue core?

Câu trả lời mong đợi: Team Vue quyết định loại bỏ Reactivity Transform vì một số lý do: thêm mental complexity bằng cách trộn hai model reactivity, yêu cầu tooling đặc biệt không phải lúc nào cũng có sẵn, và làm debugging khó hơn vì code được viết khác với code được chạy. Tính năng reactive props destructure trong Vue 3.5 giải quyết use case phổ biến nhất mà không cần macro toàn cục.

Câu hỏi 3: Làm thế nào để giữ reactivity khi destructuring?

typescript
// Câu hỏi: Sửa code này để nó reactive
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = state // Không reactive!

// Câu trả lời:
import { toRefs } from 'vue'
const state = reactive({ count: 0, name: 'Vue' })
const { count, name } = toRefs(state) // Bây giờ reactive

// Hoặc sử dụng computed cho derived values
const count = computed(() => state.count)

Câu hỏi 4: Khi nào sử dụng shallowRef vs ref?

Câu trả lời mong đợi: Sử dụng shallowRef khi làm việc với large objects hoặc arrays thường được replace toàn bộ nhưng hiếm khi bị mutate bên trong. Điều này tránh overhead từ deep reactivity tracking. Ví dụ use case: large API response, large datasets cho bảng, hoặc state được quản lý bởi external library.

Câu hỏi 5: Implement computed writable

typescript
import { ref, computed } from 'vue'

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

const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value: string) {
    const parts = value.split(' ')
    firstName.value = parts[0] || ''
    lastName.value = parts.slice(1).join(' ') || ''
  }
})

// Sử dụng
fullName.value = 'Jane Smith' // firstName = 'Jane', lastName = 'Smith'

Best Practices Reactivity Vue 3

  1. Ưu tiên ref cho composables: Trả về ref từ composables vì dễ destructure hơn và vẫn giữ reactive.

  2. Sử dụng readonly cho data không nên bị mutate: readonly(state) ngăn chặn mutation không mong muốn.

  3. Tránh deep reactivity không cần thiết: Sử dụng shallowRef hoặc markRaw cho data không cần reactive.

  4. Nhất quán với quy ước đặt tên: Thêm suffix như Ref hoặc State để rõ ràng.

  5. Tận dụng TypeScript: Định nghĩa types cho ref generics để ngăn runtime errors.

Kết luận

Mặc dù Reactivity Transform đã bị loại bỏ khỏi Vue core, việc hiểu các khái niệm đằng sau nó cung cấp insight có giá trị về hệ thống reactivity của Vue. Các tính năng hiện đại như reactive props destructure trong Vue 3.5 cung cấp ergonomics tốt hơn mà không có sự phức tạp thêm từ macro compiler. Trong phỏng vấn, khả năng giải thích các trade-off của các cách tiếp cận reactivity khác nhau thể hiện sự hiểu biết sâu sắc về Vue internals.

Thử thách hôm nay

Bạn có tìm ra lỗi trong Vue.js / Nuxt.js không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 9 tháng 9, 2026

Thẻ

#vue
#vue 3
#reactivity
#composition api
#interview

Chia sẻ

Bài viết liên quan