1. Vì sao cần học Promise?
Trong JavaScript, có những công việc hoàn thành gần như ngay lập tức:
const name = 'Doraemon';
console.log(name);Nhưng cũng có những công việc cần thời gian chờ:
Gọi Laravel REST API.
Tải danh sách bài viết từ Database thông qua API.
Đọc dữ liệu từ máy chủ.
Tải hình ảnh.
Gửi Form đăng nhập.
Chờ kết quả từ một dịch vụ bên ngoài.
Ví dụ, khi ứng dụng cần lấy danh sách bài viết từ Laravel:
JavaScript
│
│ Gửi yêu cầu
▼
Laravel REST API
│
▼
Controller
│
▼
Database
│
│ Trả kết quả
▼
JavaScript nhận dữ liệuQuá trình này có thể mất một khoảng thời gian.
JavaScript cần cơ chế xử lý công việc bất đồng bộ mà không phải chặn toàn bộ hoạt động của trình duyệt trong lúc chờ.
Hai kiến thức quan trọng chúng ta cần học là:
Promiseasync/await
2. JavaScript đồng bộ và bất đồng bộ
2.1. Đồng bộ — Synchronous
Ví dụ:
console.log('Doraemon');
console.log('Nobita');
console.log('Pikachu');Kết quả:
Doraemon
Nobita
PikachuCác câu lệnh được thực thi theo thứ tự.
2.2. Bất đồng bộ — Asynchronous
Ví dụ:
console.log('Doraemon');
setTimeout(() => {
console.log('Nobita');
}, 2000);
console.log('Pikachu');Kết quả:
Doraemon
Pikachu
NobitasetTimeout() lên lịch thực thi callback sau khoảng thời gian chỉ định.
Trong thời gian đó, JavaScript tiếp tục thực hiện các công việc khác.
Điều quan trọng cần hiểu là: bất đồng bộ không có nghĩa là JavaScript luôn chạy nhiều đoạn code cùng lúc.
JavaScript trong trình duyệt thường thực thi code trên một luồng chính, trong khi các thao tác như I/O có thể tiến hành mà không chặn luồng đó.
3. Callback là gì?
Callback là một hàm được truyền vào một hàm khác để được gọi sau đó.
Ví dụ:
function greet(name, callback) {
console.log(`Xin chào ${name}`);
callback();
}
greet('Doraemon', () => {
console.log('Bắt đầu học JavaScript');
});Kết quả:
Xin chào Doraemon
Bắt đầu học JavaScriptCallback cũng xuất hiện trong các thao tác bất đồng bộ:
console.log('Bắt đầu');
setTimeout(() => {
console.log('Đã hoàn thành');
}, 1000);
console.log('Tiếp tục công việc');Callback có thể xử lý những trường hợp đơn giản.
Tuy nhiên, khi nhiều công việc phụ thuộc lẫn nhau, code có thể trở nên khó đọc nếu lồng nhiều callback.
Đây là một trong những lý do Promise ra đời.
4. Promise là gì?
Promise là một đối tượng đại diện cho kết quả của một công việc bất đồng bộ, có thể hoàn thành thành công hoặc thất bại.
Một Promise có ba trạng thái:
|
Pending Đang chờ kết quả |
|
|
✅ Fulfilled Hoàn thành thành công |
❌ Rejected Thất bại |
Ví dụ tưởng tượng:
Doraemon gửi yêu cầu lấy bánh
│
▼
Pending
│
┌─────┴─────┐
▼ ▼
Có bánh Hết bánh
│ │
▼ ▼
Fulfilled RejectedĐây chỉ là ví dụ minh họa.
Trong ứng dụng thật, Promise có thể đại diện cho một yêu cầu API, một thao tác đọc file hoặc một công việc bất đồng bộ khác.
5. Tạo Promise đầu tiên
Chúng ta có thể tự tạo Promise bằng new Promise().
const promise = new Promise((resolve, reject) => {
const hasCake = true;
if (hasCake) {
resolve('Doraemon đã nhận được bánh!');
} else {
reject(new Error('Đã hết bánh!'));
}
});Hai hàm quan trọng:
resolve(value): hoàn thành Promise thành công.reject(error): hoàn thành Promise với trạng thái thất bại.
Để nhận kết quả, chúng ta sử dụng .then() và .catch().
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
});Kết quả:
Doraemon đã nhận được bánh!Lưu ý:
- Promise chỉ chuyển từ
pendingsang trạng thái cuối cùng một lần. - Sau khi đã fulfilled hoặc rejected, nó không chuyển sang trạng thái khác.
6. .then() — nhận kết quả thành công
Ví dụ:
const getScore = new Promise(resolve => {
resolve(100);
});
getScore.then(score => {
console.log(`Điểm của Pikachu: ${score}`);
});Kết quả:
Điểm của Pikachu: 100.then() nhận kết quả khi Promise hoàn thành thành công.
Có thể nối nhiều bước xử lý:
Promise.resolve(10)
.then(number => number * 2)
.then(number => number + 5)
.then(result => {
console.log(result);
});Kết quả:
25Mỗi .then() có thể trả về một giá trị hoặc một Promise.
Giá trị đó sẽ được chuyển sang bước tiếp theo.
Đây là cơ chế Promise chaining — nối chuỗi Promise.
7. .catch() — xử lý lỗi
Ví dụ:
const promise = new Promise((resolve, reject) => {
reject(new Error('Conan không tìm thấy dữ liệu!'));
});
promise
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
});Kết quả:
Conan không tìm thấy dữ liệu!.catch() xử lý trường hợp Promise bị reject hoặc lỗi phát sinh trong các bước xử lý trước đó của chuỗi Promise.
Trong ứng dụng Laravel, lỗi có thể xuất phát từ:
API không truy cập được.
Server trả về lỗi.
Dữ liệu không đúng định dạng mong đợi.
Code xử lý dữ liệu phát sinh exception.
Tuy nhiên, một điểm quan trọng: fetch() không tự reject Promise chỉ vì máy chủ trả về HTTP 404 hoặc 500.
Chúng ta sẽ xử lý trường hợp đó ở Bài 06.
8. .finally() — luôn thực hiện bước kết thúc
Giả sử trong lúc tải dữ liệu, giao diện hiển thị:
Đang tải dữ liệu...Khi công việc kết thúc, dù thành công hay thất bại, chúng ta muốn ẩn thông báo tải.
Có thể dùng .finally():
Promise.resolve('Dữ liệu của Doraemon')
.then(result => {
console.log(result);
})
.catch(error => {
console.error(error.message);
})
.finally(() => {
console.log('Kết thúc quá trình xử lý');
});.finally() thường được dùng để:
Ẩn trạng thái Loading.
Bật lại nút Submit.
Dọn dẹp tài nguyên.
Thực hiện công việc kết thúc.
Giá trị trả về từ .finally() thông thường không thay thế kết quả của Promise trước đó, trừ khi callback trong .finally() phát sinh lỗi hoặc trả về Promise bị reject.
9. Ví dụ Promise với setTimeout()
Tạo 01 hàm giả lập việc tải dữ liệu từ máy chủ:
function getCharacter() {
return new Promise(resolve => {
setTimeout(() => {
resolve({
name: 'Doraemon',
power: 100
});
}, 2000);
});
}
getCharacter().then(character => {
console.log(character);
});Sau khoảng hai giây, Console hiển thị:
{
name: 'Doraemon',
power: 100
}Hàm getCharacter() trả về một Promise ngay lập tức.
Giá trị dữ liệu chỉ được cung cấp khi Promise hoàn thành.
Đây là mô hình gần giống việc chờ một API trả dữ liệu.
10. Async/Await là gì?
async/await là cú pháp giúp viết code sử dụng Promise theo cách dễ đọc hơn.
Thay vì viết:
getCharacter()
.then(character => {
console.log(character.name);
})
.catch(error => {
console.error(error.message);
});Chúng ta có thể viết:
async function showCharacter() {
try {
const character = await getCharacter();
console.log(character.name);
} catch (error) {
console.error(error.message);
}
}
showCharacter();Kết quả vẫn là:
DoraemonSự khác biệt chủ yếu nằm ở cách tổ chức code.
async/await không thay thế Promise.
Nó sử dụng Promise và giúp chúng ta viết các bước bất đồng bộ dễ theo dõi hơn.
11. Từ khóa async
Khi khai báo một hàm bằng async, hàm đó luôn trả về một Promise.
Ví dụ:
async function sayHello() {
return 'Xin chào Doraemon!';
}
sayHello().then(message => {
console.log(message);
});Kết quả:
Xin chào Doraemon!Ngay cả khi chúng ta trả về một chuỗi thông thường, JavaScript cũng bọc kết quả đó trong Promise đã hoàn thành thành công.
Có thể viết Arrow Function với async:
const sayHello = async () => {
return 'Xin chào Pikachu!';
};
sayHello().then(console.log);Cú pháp này sẽ xuất hiện thường xuyên khi viết Vue và React.
12. Từ khóa await
await dùng để chờ một Promise hoàn thành và lấy kết quả của nó bên trong hàm async.
Ví dụ:
function getPower() {
return new Promise(resolve => {
setTimeout(() => {
resolve(100);
}, 1000);
});
}
async function showPower() {
console.log('Đang kiểm tra sức mạnh...');
const power = await getPower();
console.log(`Sức mạnh của Pikachu: ${power}`);
}
showPower();
console.log('JavaScript vẫn tiếp tục thực hiện công việc khác');Thứ tự kết quả:
Đang kiểm tra sức mạnh...
JavaScript vẫn tiếp tục thực hiện công việc khác
Sức mạnh của Pikachu: 100Khi chạy đến await, hàm showPower() tạm dừng tại đó để chờ Promise, nhưng luồng JavaScript vẫn có thể thực hiện những công việc khác.
Lưu ý:
awaitthường được sử dụng bên trong hàmasync.- JavaScript cũng hỗ trợ top-level
awaittrong ES Modules ở những môi trường tương thích, nhưng trong bài này chúng ta sử dụngawaitbên trong hàm để dễ tổ chức code.
13. 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.
function getCake() {
return new Promise((resolve, reject) => {
reject(new Error('Không còn bánh cho Nobita!'));
});
}
async function receiveCake() {
try {
const cake = await getCake();
console.log(cake);
} catch (error) {
console.error(error.message);
}
}
receiveCake();Kết quả:
Không còn bánh cho Nobita!Cách này giúp chúng ta tổ chức code rõ ràng hơn khi có nhiều thao tác bất đồng bộ.
Ví dụ với Laravel API:
async function loadPosts() {
try {
// Gọi API tại đây.
} catch (error) {
// Xử lý lỗi tại đây.
}
}Ở Bài 06, chúng ta sẽ thay phần giả lập bằng fetch() để gọi API thật.
14. So sánh Promise và Async/Await
| Tiêu chí | Promise chaining | Async/Await |
|---|---|---|
| Nhận kết quả | .then() | await |
| Xử lý lỗi | .catch() | try/catch |
| Dọn dẹp cuối quá trình | .finally() | Có thể dùng finally trong try/catch/finally |
| Phù hợp | Chuỗi xử lý ngắn | Quy trình nhiều bước |
| Bản chất | Promise | Vẫn sử dụng Promise |
02 cách đều hợp lệ.
Ví dụ Promise chaining:
getCharacter()
.then(character => character.name)
.then(name => console.log(name))
.catch(console.error);Ví dụ Async/Await:
async function showName() {
try {
const character = await getCharacter();
console.log(character.name);
} catch (error) {
console.error(error);
}
}
showName();Trong các bài tiếp theo, chúng ta sẽ ưu tiên async/await cho các ví dụ gọi API vì cách viết này dễ theo dõi khi có nhiều bước xử lý.
15. Chạy nhiều công việc với Promise.all()
Giả sử cần lấy thông tin của ba nhân vật.
function getCharacter(name, delay) {
return new Promise(resolve => {
setTimeout(() => {
resolve(name);
}, delay);
});
}
async function loadCharacters() {
const characters = await Promise.all([
getCharacter('Doraemon', 2000),
getCharacter('Pikachu', 1000),
getCharacter('Conan', 1500)
]);
console.log(characters);
}
loadCharacters();Kết quả:
['Doraemon', 'Pikachu', 'Conan']Thứ tự kết quả vẫn theo thứ tự các Promise được truyền vào Promise.all(), không phụ thuộc vào thứ tự hoàn thành.
Trong ví dụ này, ba công việc được bắt đầu gần như cùng lúc, nên tổng thời gian chờ thường xấp xỉ công việc lâu nhất thay vì tổng thời gian của cả ba.
Khi nào dùng Promise.all()?
Khi cần thực hiện nhiều công việc độc lập, chẳng hạn:
Tải danh sách bài viết.
Tải danh mục.
Tải thông tin thống kê.
Nếu một Promise bị reject, Promise tổng hợp từ Promise.all() cũng reject.
Nếu cần nhận kết quả thành công và thất bại riêng của từng công việc, chúng ta có thể tìm hiểu Promise.allSettled() ở phần mở rộng sau này.
16. Promise tuần tự và Promise song song
Hai cách viết dưới đây có ý nghĩa khác nhau.
Cách 1 — Chờ tuần tự
async function loadData() {
const a = await getCharacter('Doraemon', 1000);
const b = await getCharacter('Pikachu', 1000);
console.log(a, b);
}Công việc thứ hai chỉ bắt đầu sau khi công việc thứ nhất hoàn thành.
Cách 2 — Chờ song song
async function loadData() {
const promiseA = getCharacter('Doraemon', 1000);
const promiseB = getCharacter('Pikachu', 1000);
const [a, b] = await Promise.all([
promiseA,
promiseB
]);
console.log(a, b);
}Hai Promise được tạo trước khi await chờ kết quả.
Vì vậy, các công việc có thể tiến hành đồng thời.
Quy tắc:
- nếu công việc B cần kết quả của A, hãy xử lý tuần tự.
- Nếu A và B độc lập, có thể cân nhắc chạy song song.
17. Thực hành trong Laravel 13
Chúng ta sẽ tạo một trang đơn giản mô phỏng việc tải danh sách bài viết.
Ở bài này, dữ liệu vẫn là dữ liệu giả lập. Chúng ta chưa cần Laravel API thật.
Bước 1 — Tạo file resources/js/modules/posts.js
export function getPosts() {
return new Promise(resolve => {
setTimeout(() => {
resolve([
{
id: 1,
title: 'Laravel 13',
author: 'Doraemon'
},
{
id: 2,
title: 'JavaScript hiện đại',
author: 'Nobita'
},
{
id: 3,
title: 'Vue cơ bản',
author: 'Pikachu'
}
]);
}, 1500);
});
}Hàm getPosts() trả về một Promise, mô phỏng API mất khoảng 1,5 giây để trả dữ liệu.
Bước 2 — Tạo file resources/js/app.js
import { getPosts } from './modules/posts.js';
const postList = document.querySelector('#postList');
const loading = document.querySelector('#loading');
const errorMessage = document.querySelector('#errorMessage');
async function loadPosts() {
try {
if (loading) {
loading.hidden = false;
}
if (errorMessage) {
errorMessage.textContent = '';
}
const posts = await getPosts();
if (!postList) {
return;
}
postList.replaceChildren();
posts.forEach(post => {
const li = document.createElement('li');
const title = document.createElement('strong');
const author = document.createElement('span');
title.textContent = post.title;
author.textContent = ` — ${post.author}`;
li.append(title, author);
postList.append(li);
});
} catch (error) {
if (errorMessage) {
errorMessage.textContent = error.message;
}
} finally {
if (loading) {
loading.hidden = true;
}
}
}
loadPosts();Trong ví dụ này:
asynckhai báo hàm bất đồng bộ.awaitchờ dữ liệu.try/catchxử lý lỗi.finallyẩn trạng thái Loading sau khi quá trình kết thúc.
Bước 3 — Tạo Blade View
Tạo file resources/views/javascript.blade.php:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise & Async/Await</title>
@vite(['resources/js/app.js'])
</head>
<body>
<h1>Thư viện bài học</h1>
<p id="loading" hidden>
Đang tải dữ liệu...
</p>
<p id="errorMessage" role="alert"></p>
<ul id="postList"></ul>
</body>
</html>Nếu đã có trang /javascript từ những bài trước, hãy dùng lại trang đó hoặc tạo một view và Route mới để tránh khai báo Route trùng.
Bước 4 — Khởi chạy
Trong terminal thứ nhất:
npm run devTrong terminal thứ hai:
php artisan serveMở trang Laravel tương ứng trong trình duyệt.
Bạn sẽ thấy thông báo “Đang tải dữ liệu...” xuất hiện trong khoảng 1,5 giây.
Sau đó, danh sách ba bài viết sẽ hiện ra.
Đây là cách mô phỏng trạng thái Loading mà chúng ta thường gặp khi chờ dữ liệu từ API.
18. Một số lỗi thường gặp
Lỗi 1 — Quên await
async function loadPosts() {
const posts = getPosts();
console.log(posts);
}Biến posts lúc này chứa một Promise, chưa phải danh sách bài viết.
Cách đúng:
async function loadPosts() {
const posts = await getPosts();
console.log(posts);
}Lỗi 2 — Dùng await nhưng không xử lý lỗi
async function loadPosts() {
const posts = await getPosts();
console.log(posts);
}Nếu Promise bị reject, lỗi có thể trở thành một rejected Promise của hàm loadPosts() mà không được xử lý tại đây.
Nên chủ động dùng try/catch khi cần xử lý lỗi tại chỗ.
Lỗi 3 — Nghĩ rằng fetch() trả lỗi khi HTTP 404
fetch() thường vẫn hoàn thành thành công ở cấp độ Promise khi nhận được phản hồi HTTP 404 hoặc 500.
Muốn phát hiện lỗi HTTP, chúng ta cần kiểm tra thuộc tính response.ok.
Nội dung này sẽ được thực hành trong Bài 06 — Fetch API.
Lỗi 4 — Dùng await bên trong forEach()
Ví dụ không nên dùng khi muốn chờ toàn bộ công việc hoàn thành:
posts.forEach(async post => {
await savePost(post);
});forEach() không chờ các Promise do callback async trả về.
Nếu cần chạy tuần tự, có thể dùng vòng lặp for...of:
for (const post of posts) {
await savePost(post);
}Nếu các công việc độc lập và có thể chạy song song:
await Promise.all(posts.map(post => savePost(post)));Chỉ sử dụng cách thứ hai nếu hàm savePost() trả về Promise và việc chạy các thao tác song song là an toàn đối với ứng dụng.
19. Bài tập cuối bài
Bài tập 1 — Promise cơ bản
Tạo Promise trả về chuỗi:
Doraemon đã hoàn thành nhiệm vụ!Sau đó dùng .then() để in kết quả.
Bài tập 2 — Xử lý lỗi
Tạo Promise bị reject với thông báo:
Pikachu không tìm thấy năng lượng!Dùng .catch() để hiển thị lỗi.
Bài tập 3 — Async/Await
Viết hàm getCharacter() trả về Promise chứa thông tin:
{
name: 'Conan',
level: 10
}Viết hàm showCharacter() sử dụng async/await để hiển thị tên và cấp độ.
Bài tập 4 — Loading
Dựa trên ví dụ Laravel ở phần 17, bổ sung một nút “Tải lại”.
Khi bấm nút, danh sách được tải lại và trạng thái Loading được hiển thị trong lúc chờ.
Bài tập 5 — Promise.all
Tạo ba Promise trả về tên Doraemon, Nobita và Pikachu sau những khoảng thời gian khác nhau.
Dùng Promise.all() để nhận kết quả và in ra Console.
20. Tổng kết
| Kiến thức | Công dụng |
|---|---|
| Callback | Truyền hàm để thực hiện sau |
Promise | Đại diện cho kết quả của công việc bất đồng bộ |
.then() | Xử lý kết quả thành công |
.catch() | Xử lý lỗi |
.finally() | Thực hiện bước kết thúc |
async | Khai báo hàm trả về Promise |
await | Chờ Promise hoàn thành trong hàm bất đồng bộ |
try/catch | Xử lý lỗi theo cấu trúc |
Promise.all() | Chờ nhiều Promise độc lập cùng hoàn thành |
Điều cần nhớ nhất:
Promise giúp xử lý các công việc bất đồng bộ.
async/awaitlà cách viết thuận tiện hơn khi làm việc với Promise.try/catchgiúp xử lý lỗi khi chờ Promise bằngawait.Promise.all()phù hợp khi cần chờ nhiều công việc độc lập.Khi gọi API thật, cần phân biệt lỗi mạng với lỗi HTTP.
Bài 06 — Fetch API: chúng ta sẽ dùng fetch() để gửi HTTP request, nhận JSON từ Laravel REST API, hiển thị Loading và xử lý lỗi thực tế.



Không có nhận xét nào:
Đăng nhận xét