Series 3 — JavaScript Fundamentals

 

JavaScript → DOM → Async → API

 

Bài 2. Asynchronous JavaScript & API – Xử lý bất đồng bộ và giao tiếp với Server

1. Vì sao cần Asynchronous JavaScript?

Trong các website hiện đại, nhiều thao tác cần thời gian để hoàn thành, chẳng hạn như gọi API lấy danh sách sản phẩm, tải dữ liệu phim hoặc gửi thông tin đăng nhập đến server.

Nếu mọi thao tác đều phải chờ lần lượt, trải nghiệm người dùng có thể bị ảnh hưởng. Vì vậy, JavaScript cung cấp các cơ chế hỗ trợ xử lý bất đồng bộ để chương trình có thể tiếp tục thực hiện những công việc khác trong khi chờ kết quả từ các tác vụ như yêu cầu mạng hoặc bộ hẹn giờ.

Asynchronous JavaScript là nhóm kiến thức giúp chúng ta hiểu và xử lý những tác vụ không hoàn thành ngay tại thời điểm bắt đầu.

Ví dụ, khi người dùng mở một Movie App, frontend có thể gửi yêu cầu lấy danh sách phim. Trong lúc chờ server phản hồi, giao diện vẫn có thể hiển thị thông báo đang tải dữ liệu thay vì để người dùng không biết chuyện gì đang diễn ra.

2. Synchronous và Asynchronous

2.1. Synchronous – Xử lý đồng bộ

Với cách thực thi đồng bộ, các câu lệnh thông thường được xử lý theo thứ tự. Câu lệnh tiếp theo chỉ được thực hiện sau khi câu lệnh trước hoàn thành.

console.log("Bắt đầu");

console.log("Đang xử lý");

console.log("Kết thúc");

Kết quả:

Bắt đầu
Đang xử lý
Kết thúc

Đây là ví dụ đơn giản về thứ tự thực thi đồng bộ.

Nếu một thao tác đồng bộ chặn luồng thực thi chính trong thời gian dài, giao diện trình duyệt có thể bị đơ, không phản hồi thao tác người dùng.

2.2. Asynchronous – Xử lý bất đồng bộ

Với tác vụ bất đồng bộ, chương trình có thể khởi động một công việc rồi tiếp tục thực hiện những việc khác trong khi chờ kết quả.

console.log("Bắt đầu");

setTimeout(() => {
  console.log("Tác vụ bất đồng bộ");
}, 1000);

console.log("Kết thúc");

Kết quả thông thường:

Bắt đầu
Kết thúc
Tác vụ bất đồng bộ

setTimeout() yêu cầu callback được đưa vào hàng đợi sau khi khoảng thời gian tối thiểu đã trôi qua. Callback không nhất thiết được chạy chính xác sau một giây, vì còn phụ thuộc vào việc luồng thực thi có sẵn sàng xử lý nó hay không.

Ví dụ này giúp minh họa sự khác biệt giữa thứ tự gọi lệnh và thời điểm callback được thực thi.

3. Call Stack và Event Loop

Để hiểu JavaScript bất đồng bộ, cần nắm một số khái niệm nền tảng về cơ chế thực thi.

3.1. Call Stack

Call Stack là cấu trúc theo dõi các hàm đang được thực thi. Khi gọi một hàm, một execution context tương ứng được đưa vào stack; khi hàm hoàn thành, nó được loại bỏ.

function first() {
  second();
}

function second() {
  console.log("Hello JavaScript");
}

first();

Khi chạy chương trình, first() gọi second(), sau đó second() thực thi câu lệnh console.log() trước khi kết thúc.

3.2. Event Loop

Trong môi trường trình duyệt, JavaScript thường thực thi mã trên một main thread. Event Loop phối hợp việc thực thi mã với các tác vụ đang chờ, giúp callback được xử lý khi luồng thực thi có thể tiếp nhận chúng.

Một số khái niệm thường gặp:

  • Call Stack: theo dõi các hàm đang thực thi.
  • Task Queue: hàng đợi chứa những task sẵn sàng được xử lý, chẳng hạn callback của setTimeout().
  • Microtask Queue: hàng đợi ưu tiên cao hơn task thông thường, thường chứa các reaction callback của Promise.
  • Web APIs: những chức năng do trình duyệt cung cấp, chẳng hạn timer và một số cơ chế mạng.

Có thể hình dung quy trình đơn giản như sau:

  1. JavaScript thực thi mã đồng bộ trên Call Stack.
  2. Các tác vụ như timer hoặc yêu cầu mạng được khởi động thông qua cơ chế của môi trường.
  3. Khi callback hoặc kết quả bất đồng bộ sẵn sàng, công việc tương ứng được lên lịch xử lý.
  4. Event Loop phối hợp để đưa các callback vào thực thi khi phù hợp.

Đây là mô hình khái quát. Cơ chế thực tế còn phụ thuộc vào loại tác vụ và môi trường thực thi.

4. Callbacks – Hàm được truyền làm đối số

Callback là một hàm được truyền vào hàm khác để được gọi khi cần thiết.

function greet(name, callback) {
  console.log(`Xin chào ${name}`);
  callback();
}

greet("Khoa", () => {
  console.log("Chào mừng bạn đến với JavaScript");
});

Callback không nhất thiết là bất đồng bộ. Trong ví dụ trên, callback được gọi ngay trong quá trình thực thi đồng bộ của greet().

Callback cũng được sử dụng trong các thao tác bất đồng bộ:

setTimeout(() => {
  console.log("Đã hoàn thành tác vụ");
}, 1000);

Tuy nhiên, khi phải lồng nhiều callback để chờ kết quả của nhau, mã nguồn có thể trở nên khó đọc và khó xử lý lỗi. Tình trạng này thường được gọi là callback hell.

Promise và async/await là những công cụ giúp tổ chức mã bất đồng bộ dễ quản lý hơn.

5. Promise – Quản lý kết quả bất đồng bộ

Promise là một object đại diện cho kết quả có thể có của một tác vụ bất đồng bộ trong tương lai.

Promise có ba trạng thái:

  • pending: tác vụ chưa hoàn thành.
  • fulfilled: tác vụ hoàn thành thành công.
  • rejected: tác vụ thất bại.

Ví dụ:

const promise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve("Thành công");
  } else {
    reject(new Error("Có lỗi xảy ra"));
  }
});

promise
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error);
  })
  .finally(() => {
    console.log("Hoàn tất xử lý");
  });

Trong ví dụ:

  • resolve() hoàn thành Promise thành công.
  • reject() khiến Promise bị từ chối.
  • .then() xử lý kết quả thành công.
  • .catch() xử lý lỗi hoặc rejection.
  • .finally() chạy khi Promise đã kết thúc, dù thành công hay thất bại.

Promise không tự làm cho một tác vụ đồng bộ nặng trở nên bất đồng bộ. Nó là cơ chế biểu diễn và phối hợp kết quả của các tác vụ, thường được sử dụng trong những hoạt động như gọi API.

6. Async/Await – Viết mã bất đồng bộ dễ đọc hơn

async/await cung cấp cú pháp giúp viết mã làm việc với Promise theo cách gần giống mã tuần tự.

Một hàm khai báo với async luôn trả về một Promise. Từ khóa await dùng để chờ Promise hoàn thành trong phạm vi hàm bất đồng bộ, đồng thời cho phép những công việc khác tiếp tục được xử lý trong môi trường phù hợp.

function getUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: "Khoa" });
    }, 1000);
  });
}

async function displayUser() {
  const user = await getUser();

  console.log(user.name);
}

displayUser();

Trong ví dụ, await getUser() chờ Promise hoàn thành trước khi đọc user.name. Nó không có nghĩa là toàn bộ JavaScript của trình duyệt bị dừng trong một giây.

Xử lý lỗi với try…catch

Khi sử dụng await, chúng ta có thể dùng try...catch để xử lý lỗi dễ đọc hơn:

async function loadData() {
  try {
    const result = await getUser();
    console.log(result);
  } catch (error) {
    console.error("Không thể tải dữ liệu:", error);
  }
}

Trong ứng dụng thực tế, lỗi có thể xuất phát từ kết nối mạng, dữ liệu không hợp lệ hoặc server trả về kết quả không như mong đợi.

7. API là gì?

API là viết tắt của Application Programming Interface, tức giao diện lập trình ứng dụng. API định nghĩa cách các phần mềm hoặc thành phần hệ thống giao tiếp với nhau.

Trong phát triển web, khi nói đến việc frontend gọi API, chúng ta thường đề cập đến việc trình duyệt gửi HTTP request đến server để lấy hoặc gửi dữ liệu.

Ví dụ, một ứng dụng bán hàng có thể sử dụng API để:

  • Lấy danh sách sản phẩm.
  • Xem chi tiết sản phẩm.
  • Gửi thông tin đăng nhập.
  • Tạo đơn hàng.
  • Cập nhật thông tin người dùng.

Luồng xử lý khái quát:

  1. Người dùng mở trang sản phẩm.
  2. Frontend gửi HTTP request đến API.
  3. Server xử lý yêu cầu và trả về HTTP response.
  4. Frontend đọc dữ liệu trả về.
  5. JavaScript cập nhật giao diện để hiển thị kết quả.

API không nhất thiết phải trả về JSON, nhưng JSON là một định dạng phổ biến trong các ứng dụng web.

8. HTTP Methods và Status Codes

8.1. Những HTTP Methods phổ biến

HTTP method thể hiện loại thao tác mà client muốn thực hiện với tài nguyên.

Method Mục đích thường gặp
GET Lấy dữ liệu
POST Gửi dữ liệu hoặc tạo tài nguyên
PUT Thay thế trạng thái của tài nguyên
PATCH Cập nhật một phần tài nguyên
DELETE Xóa tài nguyên

Ví dụ, frontend có thể gửi GET để lấy danh sách sản phẩm và gửi POST để tạo một đơn hàng mới.

Ý nghĩa chính xác của từng thao tác còn phụ thuộc vào thiết kế API của server.

8.2. HTTP Status Codes

Status code cho biết kết quả xử lý HTTP request ở mức giao thức.

  • 200 OK: yêu cầu thành công.
  • 201 Created: tài nguyên mới đã được tạo.
  • 400 Bad Request: yêu cầu không hợp lệ.
  • 401 Unauthorized: thiếu thông tin xác thực hợp lệ hoặc thông tin xác thực không hợp lệ.
  • 403 Forbidden: server từ chối cho phép thực hiện thao tác.
  • 404 Not Found: không tìm thấy tài nguyên.
  • 500 Internal Server Error: lỗi phía server.

Khi gọi API, không nên chỉ kiểm tra xem có dữ liệu trả về hay không. Cần hiểu status code và xử lý phù hợp với từng tình huống.

9. JSON – Định dạng trao đổi dữ liệu

JSON là viết tắt của JavaScript Object Notation. Đây là định dạng văn bản được sử dụng phổ biến để trao đổi dữ liệu giữa frontend và backend.

Ví dụ:

{
  "id": 1,
  "name": "Laptop",
  "price": 15000000,
  "inStock": true
}

JSON có cú pháp gần giống object trong JavaScript nhưng không hoàn toàn giống nhau. Ví dụ, tên thuộc tính trong JSON phải được đặt trong dấu ngoặc kép và JSON không hỗ trợ trực tiếp giá trị undefined, function hoặc comment.

JavaScript cung cấp hai phương thức thường dùng:

const user = {
  name: "Khoa",
  age: 21
};

const jsonString = JSON.stringify(user);
const parsedUser = JSON.parse(jsonString);
  • JSON.stringify(): chuyển dữ liệu JavaScript phù hợp thành chuỗi JSON.
  • JSON.parse(): phân tích chuỗi JSON thành giá trị JavaScript.

Khi nhận dữ liệu từ API, bạn thường sử dụng response.json() để đọc và phân tích nội dung response dưới dạng JSON.

10. Fetch API – Gọi API bằng JavaScript

Fetch API cung cấp phương thức fetch() để thực hiện HTTP request từ trình duyệt.

Ví dụ, gọi một API công khai trả về danh sách bài viết:

async function getPosts() {
  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts"
    );

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const posts = await response.json();

    console.log(posts);
  } catch (error) {
    console.error("Không thể tải bài viết:", error);
  }
}

getPosts();

Giải thích:

  1. fetch() gửi request đến địa chỉ API và trả về một Promise.
  2. response đại diện cho HTTP response nhận được.
  3. response.ok cho biết status code có nằm trong khoảng 200–299 hay không.
  4. response.json() đọc nội dung response và phân tích JSON, trả về một Promise khác.
  5. try...catch xử lý những lỗi như lỗi mạng, lỗi khi đọc dữ liệu hoặc lỗi được chủ động ném ra.

Lưu ý quan trọng: fetch() thường không reject chỉ vì server trả về 404 hoặc 500. Vì vậy, cần kiểm tra response.ok hoặc response.status để phát hiện lỗi HTTP.

Ngoài ra, API công khai có thể gặp giới hạn truy cập, lỗi mạng hoặc chính sách CORS tùy môi trường. Nếu ví dụ không chạy, hãy kiểm tra kết nối và phản hồi từ server.

11. Hiển thị dữ liệu API lên giao diện DOM

Lấy được dữ liệu từ API mới chỉ là một phần của công việc. Frontend còn cần chuyển dữ liệu thành các phần tử HTML để người dùng có thể xem và tương tác.

Ví dụ, tạo danh sách bài viết từ API:

<h1>Danh sách bài viết</h1>
<p id="status">Đang tải dữ liệu...</p>
<ul id="post-list"></ul>
const status = document.querySelector("#status");
const postList = document.querySelector("#post-list");

async function renderPosts() {
  try {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/posts?_limit=5"
    );

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const posts = await response.json();

    postList.replaceChildren();

    posts.forEach((post) => {
      const item = document.createElement("li");
      const title = document.createElement("h2");
      const body = document.createElement("p");

      title.textContent = post.title;
      body.textContent = post.body;

      item.append(title, body);
      postList.appendChild(item);
    });

    status.textContent = posts.length
      ? ""
      : "Không có bài viết nào.";
  } catch (error) {
    status.textContent = "Không thể tải dữ liệu. Vui lòng thử lại.";
    console.error(error);
  }
}

renderPosts();

Ví dụ này kết hợp nhiều kiến thức:

  • async/await để xử lý bất đồng bộ.
  • fetch() để gửi HTTP request.
  • response.json() để đọc dữ liệu.
  • forEach() để duyệt danh sách.
  • createElement() và appendChild() để tạo giao diện.
  • textContent để hiển thị nội dung văn bản an toàn hơn so với đưa dữ liệu trực tiếp vào innerHTML.

Trong ứng dụng thực tế, bạn cũng nên quản lý trạng thái loading, thông báo lỗi, danh sách rỗng và khả năng người dùng thử tải lại dữ liệu.

12. Những lưu ý khi làm việc với API

Không giả định request luôn thành công

Kết nối mạng có thể thất bại, server có thể gặp lỗi hoặc API có thể trả về dữ liệu khác với dự kiến. Hãy kiểm tra response và xử lý lỗi.

Phân biệt lỗi mạng với lỗi HTTP

Lỗi mạng có thể khiến fetch() reject. Ngược lại, status code như 404 thường vẫn tạo ra một response, nên cần kiểm tra response.ok.

Không gọi API lặp lại không cần thiết

Các request trùng lặp có thể làm ứng dụng chậm và gây áp lực lên server. Hãy tổ chức logic tải dữ liệu phù hợp, đặc biệt khi giao diện có nhiều component.

Chú ý bảo mật

Không đặt secret key hoặc thông tin xác thực nhạy cảm trong mã JavaScript chạy trên trình duyệt. Mã phía client có thể được người dùng kiểm tra. Những thông tin cần giữ bí mật nên được quản lý ở môi trường server phù hợp.

Hiểu CORS

CORS là cơ chế bảo mật của trình duyệt liên quan đến việc trang web truy cập tài nguyên từ một origin khác. Nếu request bị chặn bởi CORS, việc chỉ sửa JavaScript phía frontend thường không đủ; server cần cấu hình phản hồi phù hợp.

Không nhầm API với cơ sở dữ liệu

Frontend thường giao tiếp với server thông qua API, còn server chịu trách nhiệm xử lý nghiệp vụ và làm việc với cơ sở dữ liệu. Trình duyệt không nên truy cập trực tiếp cơ sở dữ liệu nội bộ của ứng dụng.

Kết luận

Asynchronous JavaScript giúp ứng dụng xử lý những tác vụ mất thời gian mà không cần viết toàn bộ chương trình theo kiểu chờ tuần tự. Các khái niệm như Callback, Promise, async/await và Event Loop giúp bạn hiểu rõ hơn cách JavaScript phối hợp các công việc bất đồng bộ.

Trong khi đó, API là cầu nối để frontend giao tiếp với server, còn Fetch API giúp JavaScript gửi request và nhận dữ liệu qua HTTP. Khi kết hợp với JSON và DOM, bạn có thể xây dựng những ứng dụng hiển thị dữ liệu thực tế thay vì chỉ sử dụng nội dung HTML tĩnh.

Đối với Frontend Developer, việc hiểu cách gọi API, kiểm tra response, quản lý trạng thái giao diện và xử lý lỗi là nền tảng quan trọng trước khi chuyển sang React, Next.js và các ứng dụng web phức tạp hơn.

Bài viết khác

Transactions & ACID

1. Transaction là gì? Transaction (giao dịch) là một nhóm thao tác trên database được xem như một đơn vị xử lý logic. Mục tiêu là bảo đảm những thay đổi liên quan được thực hiện theo cách nhất quán: nếu giao dịch thành công thì các thay đổi cần thiết được xác nhận; nếu […]

Database Normalization – 1NF, 2NF, 3NF & Denormalization

1. Database Normalization là gì? Database Normalization (chuẩn hóa dữ liệu) là quá trình tổ chức dữ liệu thành các bảng và quan hệ hợp lý nhằm giảm dư thừa không cần thiết, tránh dữ liệu mâu thuẫn và duy trì tính toàn vẹn của database. Khi thiết kế database, nếu lưu tất cả thông […]

Database Design & Relationships

1. Database Design là gì? Database Design là quá trình thiết kế cách tổ chức, lưu trữ và liên kết dữ liệu trong một hệ thống sao cho dữ liệu có cấu trúc hợp lý, hạn chế trùng lặp không cần thiết, bảo đảm tính toàn vẹn và hỗ trợ các truy vấn mà application […]

PostgreSQL / MySQL

1. PostgreSQL và MySQL là gì? PostgreSQL và MySQL là hai hệ quản trị cơ sở dữ liệu quan hệ (Relational Database Management System – RDBMS), được sử dụng để lưu trữ, truy vấn và quản lý dữ liệu cho các ứng dụng. Tuy cùng thuộc nhóm relational database, PostgreSQL và MySQL có sự khác […]

Sanity Testing(Kiểm thử sự hợp lý)

Sanity Testing là gì? Trong quá trình phát triển và vận hành một dự án phần mềm, hoạt động lập trình không bao giờ dừng lại ở lần phát hành đầu tiên. Sau khi nhận được phản hồi từ đội ngũ QC hoặc người dùng thực tế, các lập trình viên sẽ liên tục tiến […]

Laravel Routing & Controllers

Laravel Routing và Controllers là gì? 1. Laravel Routing là gì? Laravel Routing (định tuyến trong Laravel) là cơ chế xác định cách ứng dụng tiếp nhận và xử lý HTTP request dựa trên những thông tin như HTTP method và URL. Một route có thể chuyển request đến Controller, thực thi Closure (hàm ẩn […]

Leave a Reply

Your email address will not be published. Required fields are marked *