ShallowRef và ShallowReactive: Khi nào "nông" lại là "sâu" trong Vue.js?

ShallowRef và ShallowReactive: Khi nào "nông" lại là "sâu" trong Vue.js?

Giới thiệu: Khai thác sức mạnh "nông" của Reactivity trong Vue

Chào các bạn, những người đồng hành cùng tôi trên hành trình khám phá Vue.js! Chúng ta đều biết reactivity (tính phản ứng) là một trong những "phép màu" của Vue, giúp dữ liệu thay đổi là giao diện cập nhật ngay lập tức. Thông thường, chúng ta sử dụng ref() cho giá trị đơn lẻ và reactive() cho các đối tượng phức tạp, và Vue sẽ "phù phép" để theo dõi mọi ngóc ngách của dữ liệu.

Nhưng đôi khi, sự "sâu sắc" này lại không phải lúc nào cũng là điều tốt. Với các cấu trúc dữ liệu khổng lồ hoặc khi tích hợp với thư viện bên ngoài, việc Vue cố gắng theo dõi mọi thứ có thể trở thành gánh nặng hiệu suất. Đó là lúc chúng ta cần đến những người anh em "nông" hơn: shallowRefshallowReactive.

Vậy, khi nào "nông" lại là "sâu"? Hãy cùng tìm hiểu!

ShallowRef: Chỉ quan tâm đến lớp vỏ ngoài

Giống như tên gọi, shallowRef tạo ra một ref "nông". Điều này có nghĩa là gì?

  • Nó chỉ theo dõi sự thay đổi của chính thuộc tính .value của ref đó.
  • Nó KHÔNG theo dõi các thay đổi bên trong đối tượng hoặc mảng mà .value đang giữ.

Khi nào nên dùng ShallowRef?

  • Tối ưu hiệu suất với đối tượng lớn, bất biến: Khi bạn có một đối tượng dữ liệu khổng lồ và bạn biết rằng mình sẽ chỉ thay thế toàn bộ đối tượng đó (thay vì sửa từng thuộc tính con), shallowRef sẽ hiệu quả hơn nhiều. Vue không cần tạo proxy cho tất cả các thuộc tính lồng sâu, tiết kiệm bộ nhớ và CPU.
  • Tích hợp với thư viện bên ngoài: Nếu bạn đang làm việc với một đối tượng được quản lý bởi một thư viện JavaScript khác (ví dụ: một đối tượng scene của Three.js, một biểu đồ D3.js), bạn có thể muốn Vue chỉ theo dõi khi bạn gán một đối tượng mới hoàn toàn, tránh xung đột hoặc proxying không cần thiết.

Ví dụ minh họa ShallowRef

Hãy xem ví dụ sau:

<template>  <p>User Name: {{ user.name }}</p>  <p>User Age: {{ user.age }}</p>  <button @click="changeUserName">Change User Name (Doesn't update)</button>  <button @click="replaceUser">Replace User Object (Updates)</button></template><script setup>import { shallowRef, triggerRef } from 'vue';const user = shallowRef({ name: 'Alice', age: 30 });const changeUserName = () => {  user.value.name = 'Bob'; // Thay đổi thuộc tính bên trong, KHÔNG kích hoạt cập nhật giao diện  console.log(user.value);  // Nếu bạn muốn ép buộc cập nhật, bạn có thể dùng triggerRef()  // triggerRef(user);};const replaceUser = () => {  user.value = { name: 'Charlie', age: 25 }; // Thay thế toàn bộ đối tượng, KÍCH HOẠT cập nhật giao diện  console.log(user.value);};</script>

Trong ví dụ trên, khi bạn bấm "Change User Name", giao diện sẽ không cập nhật vì user.value không bị thay thế. Nhưng khi bạn bấm "Replace User Object", giao diện sẽ cập nhật vì bạn đã gán một đối tượng mới cho user.value.

ShallowReactive: Sức mạnh ở các thuộc tính cấp 1

Tương tự như shallowRef, shallowReactive cũng tạo ra một đối tượng reactive "nông".

  • Nó chỉ theo dõi các thay đổi của các thuộc tính cấp cao nhất của đối tượng.
  • Nó KHÔNG theo dõi các thay đổi của các thuộc tính lồng sâu bên trong các đối tượng con.

Khi nào nên dùng ShallowReactive?

  • Tối ưu hiệu suất cho đối tượng lớn với các thuộc tính cấp 1 thường xuyên thay đổi: Nếu bạn có một đối tượng phức tạp nhưng bạn chỉ quan tâm đến việc thay đổi các thuộc tính trực tiếp của nó (ví dụ: gán một đối tượng con mới cho một thuộc tính cấp 1), shallowReactive sẽ là lựa chọn phù hợp.
  • Tránh proxying sâu không cần thiết: Giúp tiết kiệm tài nguyên khi không cần thiết phải theo dõi mọi cấp độ lồng ghép của dữ liệu.

Ví dụ minh họa ShallowReactive

Hãy xem ví dụ sau:

<template>  <p>User Info: {{ userInfo.name }} - {{ userInfo.details.city }}</p>  <button @click="changeCity">Change City (Doesn't update)</button>  <button @click="changeName">Change Name (Updates)</button>  <button @click="replaceDetails">Replace Details (Updates)</button></template><script setup>import { shallowReactive } from 'vue';const userInfo = shallowReactive({  name: 'David',  age: 40,  details: {    city: 'Hanoi',    country: 'Vietnam'  }});const changeCity = () => {  userInfo.details.city = 'Ho Chi Minh'; // Thay đổi thuộc tính lồng sâu, KHÔNG kích hoạt cập nhật giao diện  console.log(userInfo.details.city);};const changeName = () => {  userInfo.name = 'Eve'; // Thay đổi thuộc tính cấp 1, KÍCH HOẠT cập nhật giao diện  console.log(userInfo.name);};const replaceDetails = () => {  userInfo.details = { city: 'London', country: 'UK' }; // Thay thế thuộc tính cấp 1 (details), KÍCH HOẠT cập nhật giao diện  console.log(userInfo.details);};</script>

Ở đây, thay đổi userInfo.details.city sẽ không làm giao diện cập nhật vì nó là một thuộc tính lồng sâu. Tuy nhiên, thay đổi userInfo.name hoặc gán một đối tượng mới cho userInfo.details (một thuộc tính cấp 1) sẽ kích hoạt cập nhật.

Kết luận: Khi nào "nông" là lựa chọn tối ưu?

Mặc định, bạn nên luôn sử dụng refreactive bởi chúng cung cấp reactivity đầy đủ và an toàn nhất. Tuy nhiên, shallowRefshallowReactive là những công cụ mạnh mẽ khi bạn cần kiểm soát chặt chẽ hơn cơ chế reactivity của Vue để:

  • Tối ưu hiệu suất: Với các cấu trúc dữ liệu lớn, phức tạp mà bạn thường xuyên thay thế toàn bộ hoặc chỉ chỉnh sửa các thuộc tính cấp cao nhất.
  • Tích hợp với thư viện ngoài: Khi bạn muốn Vue chỉ theo dõi ở một mức độ nhất định, để tránh xung đột hoặc proxying không cần thiết với các đối tượng được quản lý bởi thư viện bên thứ ba.
  • Giảm gánh nặng bộ nhớ và CPU: Bằng cách tránh tạo các proxy sâu không cần thiết.

Hãy nhớ, việc lựa chọn "nông" hay "sâu" phụ thuộc vào yêu cầu cụ thể của ứng dụng và cấu trúc dữ liệu của bạn. Hiểu rõ từng công cụ sẽ giúp bạn xây dựng những ứng dụng Vue không chỉ mạnh mẽ mà còn mượt mà và hiệu quả!