Vue.js 3 · Phần 1 — Mental Model & App đầu tiên
Bắt đầu Vue 3 đúng cách: reactivity là gì và vì sao nó là trái tim của Vue, Single-File Component, dựng project với Vite, và viết app đầu tiên với <script setup> + Composition API.
Vue là một framework để xây UI dựa trên trạng thái (state). Bạn mô tả “với state này thì DOM trông như thế nào”, còn Vue lo phần đồng bộ DOM mỗi khi state đổi. Toàn bộ series này xoay quanh một ý tưởng: reactivity — và nếu nắm chắc nó ngay từ phần 1, mọi thứ về sau (computed, watch, component, Pinia) sẽ thành hệ quả tự nhiên.
1. Vấn đề Vue giải quyết
Không có framework, bạn tự đồng bộ state và DOM bằng tay:
let count = 0;
const btn = document.querySelector('#btn');
const label = document.querySelector('#label');
btn.addEventListener('click', () => {
count++;
label.textContent = `Count: ${count}`; // nhớ cập nhật DOM tay
});
Vấn đề: mỗi nơi đổi count bạn phải nhớ cập nhật đúng phần DOM. App lớn lên, việc này nhân lên và sinh bug “state nói một đằng, màn hình hiển thị một nẻo”.
Vue đảo ngược việc đó. Bạn khai báo quan hệ, không phải thao tác:
<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<button @click="count++">Count: {{ count }}</button>
</template>
Bạn không bao giờ chạm DOM. Khi count đổi, Vue tự cập nhật đúng chữ cần đổi. Đó là declarative rendering.
2. Reactivity — trái tim của Vue
Mental model cốt lõi: Vue theo dõi (track) chỗ nào đọc một state reactive, và kích hoạt (trigger) cập nhật đúng những chỗ đó khi state ghi.
ref(0) ──đọc trong template──▶ Vue ghi nhớ "template phụ thuộc count"
count++ ──ghi──▶ Vue chạy lại đúng phần render phụ thuộc count
ref() bọc một giá trị vào một object có property .value. Vue chặn việc đọc/ghi .value để biết ai phụ thuộc nó:
import { ref } from 'vue';
const count = ref(0);
console.log(count.value); // đọc → 0
count.value++; // ghi → trigger cập nhật
Trong
<template>bạn viếtcount(không cần.value) vì Vue tự “unwrap” ref ở tầng cao nhất. Trong<script>thì luôn cần.value. Đây là điểm gây lú phổ biến nhất với người mới — ghi nhớ: template không.value, script có.value.
So với React: React render lại cả component khi state đổi rồi diff Virtual DOM. Vue 3 theo dõi phụ thuộc ở mức fine-grained — chỉ chạy lại đúng effect liên quan. Ta sẽ đào sâu cơ chế này ở Phần 3.
3. Single-File Component (SFC)
Đơn vị xây dựng của Vue là file .vue — gom logic, template và style của một component vào một chỗ:
<script setup>
// logic — chạy một lần khi component khởi tạo
import { ref } from 'vue';
const message = ref('Xin chào Vue');
</script>
<template>
<!-- markup — khai báo UI theo state -->
<h2>{{ message }}</h2>
</template>
<style scoped>
/* style — `scoped` giới hạn CSS trong component này */
h2 { color: #42b883; }
</style>
Ba phần này không bị tách rải rác qua nhiều file — đọc một component là hiểu trọn nó. <style scoped> đảm bảo CSS không rò ra ngoài. <script setup> là cú pháp gọn nhất của Composition API và là cách viết khuyến nghị cho Vue 3.
4. Dựng project với Vite
Vue chính thức scaffold bằng create-vue (chạy trên Vite). Đây là cách bắt đầu chuẩn cho dự án mới:
npm create vue@latest my-app
# chọn: TypeScript ✓, Router (sau), Pinia (sau), Vitest (sau)
cd my-app
npm install
npm run dev
Cấu trúc tối thiểu sinh ra:
my-app/
├── index.html # điểm vào, mount #app
├── vite.config.ts # @vitejs/plugin-vue
└── src/
├── main.ts # tạo app và mount
├── App.vue # root component
└── components/
src/main.ts là nơi app khởi động:
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app'); // gắn App vào <div id="app"> trong index.html
createApp trả về một application instance — về sau ta .use() plugin (Router, Pinia) trên đó trước khi .mount().
5. App đầu tiên thật sự — bộ đếm có ràng buộc
Gom lại tất cả: state, sự kiện, và một giá trị dẫn xuất.
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
const isEven = computed(() => count.value % 2 === 0); // dẫn xuất từ count
function reset() {
count.value = 0;
}
</script>
<template>
<div class="counter">
<p>Count: {{ count }} — {{ isEven ? 'chẵn' : 'lẻ' }}</p>
<button @click="count++">+1</button>
<button @click="count--">-1</button>
<button @click="reset">Reset</button>
</div>
</template>
Để ý ba thứ nền tảng đã xuất hiện:
refcho state thay đổi được.computedcho giá trị dẫn xuất —isEventự tính lại khicountđổi, và được cache khicountkhông đổi.@click(viết tắt củav-on:click) gắn sự kiện DOM vào logic.
Bạn không gọi “re-render” ở đâu cả. Sửa count.value là đủ.
6. Composition API vs Options API
Bạn sẽ gặp code Vue cũ viết theo Options API (data(), methods, computed là các object):
<script>
export default {
data() { return { count: 0 }; },
methods: { inc() { this.count++; } },
};
</script>
Series này dùng Composition API (<script setup>) vì nó: gom logic liên quan lại gần nhau (thay vì rải qua các “option”), tái dùng logic dễ qua composable (Phần 5), và type-safe hơn hẳn với TypeScript. Options API vẫn chạy và không bị deprecate, nhưng cho dự án mới Composition API là lựa chọn mặc định.
7. Bài tập
1. Vì sao trong <script> phải dùng count.value còn trong <template> thì không?
Lời giải
ref() trả về một object { value }. Trong template, Vue tự unwrap ref ở tầng cao nhất nên count đủ. Trong script bạn cầm chính object đó, phải qua .value để đọc/ghi giá trị bên trong (và để Vue track/trigger).
2. Thêm một computed tên doubled trả về count * 2 và hiển thị nó.
Lời giải
const doubled = computed(() => count.value * 2);<p>Doubled: {{ doubled }}</p>3. Nếu thay const count = ref(0) bằng let count = 0 thì nút +1 còn cập nhật màn hình không? Vì sao?
Lời giải
Không. Một biến thường không reactive — Vue không track việc đọc và không trigger render khi ghi. Reactivity bắt buộc đi qua ref/reactive.
Điểm chính
- Vue là declarative: mô tả UI theo state, không thao tác DOM tay.
- Reactivity là cốt lõi — Vue track nơi đọc state và trigger cập nhật đúng chỗ khi ghi.
ref()tạo state reactive; template không.value, script có.value.- SFC gom logic + template + style;
<script setup>là Composition API gọn nhất. - Dựng dự án bằng
create-vuetrên Vite;createApp(App).mount('#app').
Phần tiếp theo
Ta đã ràng buộc state ra template bằng {{ }} và @click. Phần 2 — Cú pháp template & directive đi đủ bộ công cụ khai báo UI: v-bind cho attribute, v-if/v-show cho điều kiện, v-for cho danh sách (và vì sao cần key), v-on cho sự kiện với modifier, và v-model cho ràng buộc hai chiều.