jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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ết count (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:

  • ref cho state thay đổi được.
  • computed cho giá trị dẫn xuấtisEven tự tính lại khi count đổi, và được cache khi count không đổi.
  • @click (viết tắt của v-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-vue trên Vite; createApp(App).mount('#app').

Phần tiếp theo

Ta đã ràng buộc state ra template bằng {{ }}@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.