Trong các ứng dụng Vue, việc sử dụng biểu thức trực tiếp trong template là tiện lợi cho các thao tác đơn giản. Tuy nhiên, khi logic trở nên phức tạp hơn, việc nhúng quá nhiều mã nguồn vào template có thể khiến chúng trở nên khó đọc, khó bảo trì và dễ gây lỗi. Để giải quyết vấn đề này, Vue 3 cung cấp một tính năng mạnh mẽ gọi là Thuộc tính Tính toán (Computed Properties).
Vấn đề với logic phức tạp trong Template
Hãy xem xét một ví dụ đơn giản. Chúng ta có một đối tượng đại diện cho một tác giả và một danh sách các đầu sách mà người đó đã viết. Chúng ta muốn hiển thị một thông báo cho biết liệu tác giả có bất kỳ cuốn sách nào hay không.
<script lang="ts" setup>
import { reactive } from "vue";
const writerInfo = reactive({
name: 'Nguyễn Văn A',
publishedWorks: [
'Vượt qua sóng gió',
'Hành trình phương Nam',
'Bình minh trên biển'
]
});
</script>
<template>
<div class="content-wrapper">
<p>Tác giả đã xuất bản sách:</p>
<span>{{ writerInfo.publishedWorks.length > 0 ? 'Có' : 'Không' }}</span>
</div>
</template>
<style lang="scss" scoped>
.content-wrapper {}
</style>
Trong ví dụ trên, biểu thức điều kiện writerInfo.publishedWorks.length > 0 ? 'Có' : 'Không' được viết trực tiếp trong template. Mặc dù đoạn mã này khá ngắn, nhưng đối với các logic phức tạp hơn, template sẽ nhanh chóng trở nên lộn xộn.
Sử dụng Computed Properties để làm sạch Template
Để giữ cho template của chúng ta gọn gàng và dễ đọc, chúng ta nên chuyển logic phức tạp vào một thuộc tính tính toán. Thuộc tính tính toán là một giá trị được dẫn xuất từ trạng thái phản ứng (reactive state) khác.
<script lang="ts" setup>
import { reactive, computed } from "vue";
const writerInfo = reactive({
name: 'Nguyễn Văn A',
publishedWorks: [
'Vượt qua sóng gió',
'Hành trình phương Nam',
'Bình minh trên biển'
]
});
// Định nghĩa một thuộc tính tính toán
const hasPublishedBooks = computed(() => {
return writerInfo.publishedWorks.length > 0 ? 'Có' : 'Không';
});
</script>
<template>
<div class="content-wrapper">
<p>Tác giả đã xuất bản sách:</p>
<span>{{ hasPublishedBooks }}</span>
</div>
</template>
<style lang="scss" scoped>
.content-wrapper {}
</style>
Trong đoạn mã này:
- Chúng ta sử dụng hàm
computed()từ Vue để tạo một thuộc tính tính toán có tênhasPublishedBooks. computed()nhận một hàm getter làm đối số. Hàm này sẽ chứa logic tính toán của chúng ta và trả về giá trị mong muốn.- Giá trị trả về từ
computed()là một ref tính toán. Trong template, các ref này tự động được giải nén, vì vậy chúng ta có thể sử dụng trực tiếp{{ hasPublishedBooks }}mà không cần.value. - Điều quan trọng là Vue tự động theo dõi các phụ thuộc phản ứng của
hasPublishedBooks. NếuwriterInfo.publishedWorksthay đổi (ví dụ, thêm hoặc bớt sách),hasPublishedBookssẽ tự động được tính toán lại và giao diện người dùng sẽ cập nhật.
Computed Properties và Phương thức (Methods): Sự khác biệt về Cache
Bạn có thể tự hỏi tại sao không sử dụng một phương thức để đạt được kết quả tương tự:
...
<script lang="ts" setup>
import { reactive } from "vue";
const writerInfo = reactive({
name: 'Nguyễn Văn A',
publishedWorks: [
'Vượt qua sóng gió',
'Hành trình phương Nam',
'Bình minh trên biển'
]
});
function checkPublishedStatus() {
return writerInfo.publishedWorks.length > 0 ? 'Có' : 'Không';
}
</script>
<template>
<div class="content-wrapper">
<p>Tác giả đã xuất bản sách:</p>
<span>{{ checkPublishedStatus() }}</span>
</div>
</template>
...
Mặc dù cả hai cách đều cho cùng một kết quả, nhưng có một sự khác biệt quan trọng:
- Tính năng Cache của Computed Properties: Giá trị của một thuộc tính tính toán được lưu vào bộ nhớ cache dựa trên các phụ thuộc phản ứng của nó. Điều này có nghĩa là
hasPublishedBookschỉ được tính toán lại khiwriterInfo.publishedWorksthay đổi. NếuwriterInfo.publishedWorkskhông đổi, mỗi lần truy cậphasPublishedBookssẽ trả về ngay kết quả đã được lưu trong cache mà không cần thực hiện lại hàm getter. - Phương thức (Methods): Ngược lại, một phương thức như
checkPublishedStatus()sẽ luôn được thực thi lại mỗi khi thành phần được render lại, bất kể các phụ thuộc của nó có thay đổi hay không.
Tính năng cache của computed properties là rất quan trọng cho hiệu suất. Nếu bạn có một phép tính tốn kém (ví dụ: lặp qua một mảng lớn, thực hiện nhiều phép toán), việc sử dụng computed property sẽ ngăn chặn việc lặp lại các phép tính không cần thiết, đặc biệt khi các thuộc tính khác trong thành phần thay đổi và kích hoạt một lần render lại.
Thuộc tính Tính toán Có thể Ghi (Writable Computed Properties)
Theo mặc định, các thuộc tính tính toán là chỉ đọc (read-only). Nếu bạn cố gắng gán một giá trị mới cho một computed property, bạn sẽ nhận được một cảnh báo lỗi trong console. Tuy nhiên, trong một số trường hợp đặc biệt, bạn có thể cần một thuộc tính tính toán có thể ghi. Để làm điều này, bạn phải cung cấp cả hàm getter và setter:
<script setup lang="ts">
import { ref, computed } from 'vue';
const hoDem = ref('Lê');
const tenChinh = ref('Văn Luyện');
const hoVaTenDayDu = computed({
// Hàm getter để đọc giá trị
get() {
return hoDem.value + ' ' + tenChinh.value;
},
// Hàm setter để ghi giá trị
set(newFullName) {
// Chia chuỗi thành họ đệm và tên chính
const parts = newFullName.split(' ');
if (parts.length >= 2) {
hoDem.value = parts.slice(0, parts.length - 1).join(' ');
tenChinh.value = parts[parts.length - 1];
} else {
// Xử lý trường hợp chuỗi không đúng định dạng
hoDem.value = '';
tenChinh.value = newFullName;
}
}
});
const capNhatTen = () => {
// Khi gán giá trị cho hoVaTenDayDu, hàm setter sẽ được gọi
hoVaTenDayDu.value = 'Trần Thị Thu Thảo';
};
</script>
<template>
<div class="content-wrapper">
<button class="button" @click="capNhatTen">Đổi Tên</button>
<p>Họ và Tên: {{ hoVaTenDayDu }}</p>
</div>
</template>
<style lang="scss" scoped>
.content-wrapper {}
</style>
Trong ví dụ này, khi nút "Đổi Tên" được nhấn, capNhatTen sẽ thực thi hoVaTenDayDu.value = 'Trần Thị Thu Thảo'. Điều này kích hoạt hàm set() của hoVaTenDayDu, phân tích chuỗi mới và cập nhật các ref hoDem và tenChinh tương ứng. Khi hoDem và tenChinh thay đổi, hàm get() của hoVaTenDayDu sẽ được tính toán lại, và giao diện người dùng sẽ hiển thị tên mới.
Tuy nhiên, việc sử dụng computed properties có thể ghi là một trường hợp hiếm gặp. Thông thường, bạn nên cập nhật các trạng thái nguồn mà computed property phụ thuộc vào để kích hoạt một lần tính toán mới, thay vì cố gắng sửa đổi trực tiếp computed property.
Các Thực hành Tốt nhất
-
Getter không nên gây ra Tác dụng phụ (Side Effects): Hàm getter của một computed property chỉ nên tập trung vào việc tính toán và trả về giá trị. Nó không nên thực hiện các hoạt động gây ra tác dụng phụ như thay đổi DOM, gửi yêu cầu HTTP, hay cập nhật các trạng thái không liên quan. Các tác dụng phụ nên được xử lý bằng watchers (bộ theo dõi).
-
Tránh sửa đổi trực tiếp giá trị của Computed Property: Giá trị trả về từ một computed property là một trạng thái dẫn xuất, giống như một "ảnh chụp nhanh" tạm thời. Việc thay đổi ảnh chụp nhanh này không có ý nghĩa. Thay vào đó, bạn nên thay đổi các trạng thái nguồn mà computed property phụ thuộc vào để kích hoạt một lần tính toán mới.