Bài trước: Web nâng cao (7) - JavaScript cho React (6)
-----1.1.1
Lập trình bất đồng bộ bằng callback
Trong JavaScript có 3 kĩ thuật để lập trình bất đồng bộ gồm:
callback, promise và async/await.
Để hiểu về kĩ thuật lập trình bất đồng bộ bằng callback,
chúng ta sẽ cùng làm một chức năng đơn giản là lấy dữ liệu từ Internet về trình
duyệt.
Bạn vào trang Google, gõ từ khóa JSON placeholder để tìm tới
trang web cho phép bạn có thể truy cập và lấy dữ liệu dạng JSON về trình duyệt.
Ví dụ trang: https://jsonplaceholder.typicode.com/
Trang JSON Placeholder cho phép bạn lấy dữ liệu về dưới dạng
lời gọi API, bạn có thể nhập trực tiếp các lời gọi API vào thanh địa chỉ của
trình duyệt để lấy dữ liệu về, ví dụ:
https://jsonplaceholder.typicode.com/todos/1
/*
{
"userId": 1,
"id": 1,
"title": "delectus aut
autem",
"completed": false
}
*/
Bạn có thể thay đổi giá trị Id của lời gọi API thành 2, 3, 4
hoặc “rỗng” để xem kết quả.
https://jsonplaceholder.typicode.com/todos/2
https://jsonplaceholder.typicode.com/todos/3
https://jsonplaceholder.typicode.com/todos/4
https://jsonplaceholder.typicode.com/todos/
Trong JavaScript bạn có 2 cách để tạo một request là sử dụng
đối tượng XMLHttpRequest và Fetch API. Phần này sẽ thực hành sử dụng đối tượng XMLHttpRequest.
Để tạo đối tượng request, bạn sử dụng lệnh sau:
const request = new XMLHttpRequest();
Tùy từng thời điểm, request sẽ có các trạng thái sau:
Giá trị |
Trạng thái |
Mô tả |
0 |
UNSENT |
Đã tạo ra đối tượng request, chưa gọi phương thức open() |
1 |
OPENED |
Đã gọi phương thức open() |
2 |
HEADER_RECEIVED |
Đã gọi phương thức send(), đã có thông tin các header và
trạng thái |
3 |
LOADING |
Đang tải dữ liệu về máy client, dữ liệu chứa trong thuộc
tính responseText |
4 |
DONE |
Hoàn thành một phiên gửi request |
Đoạn mã nguồn sau đây viết theo kiểu tuần tự để lấy dữ liệu
từ server về client, đồng thời kiểm tra giá trị của trạng thái request:
// khai báo và tạo request
const request = new XMLHttpRequest();
console.log(request, request.readyState);
request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
console.log(request, request.readyState);
// gửi request tới server
request.send();
// xuất ngay kết quả do server trả về
console.log(request, request.readyState); // chưa có dữ liệu do server chưa trả về kịp
Rõ ràng bạn thấy cách viết theo kiểu tuần tự khá bị động, rất
khó để biết khi nào thì client nhận đủ dữ liệu của server để thực hiện các xử
lý tiếp theo.
Bạn có thể viết theo kiểu bất đồng bộ bằng cách dùng hàm setTimeout(), tuy nhiên bạn phải biết được
“khi nào thì client nhận đủ dữ liệu từ server?”.
// khai báo và tạo request
const request = new XMLHttpRequest();
console.log(request, request.readyState);
request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
console.log(request, request.readyState);
// gửi request tới server
request.send();
// xuất ngay kết quả do server trả về
console.log(request, request.readyState); // chưa có dữ liệu do server chưa trả về kịp
// chờ đủ lâu để xuất kết quả do server trả về
setTimeout(() => console.log(request, request.readyState), 4000);
Có cách hay hơn để biết “khi nào client nhận đủ dữ liệu” là
dựa vào sự kiện readystatechange. Sự
kiện readystatechange sẽ phát sinh mỗi
khi đối tượng request có thay đổi trạng thái, khi trạng thái thay đổi bạn sẽ gọi
một hàm callback để xử lý. Đồng thời bạn cần kiểm tra khi nào request.readyState
có giá trị là 4 và client thực sự nhận được dữ liệu từ server (status = 200) thì
mới thực hiện các xử lý trên dữ liệu:
// khai báo và tạo request
const request = new XMLHttpRequest();
// gắn bộ lắng nghe sự kiện readystatechange cho đối tượng request
request.addEventListener('readystatechange', () => {
if(request.readyState === 4 && request.status === 200){
console.log(request.responseText);
} else if (request.readyState === 4 ){
console.log("Không nhận được dữ liệu từ server!");
}
});
request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
// gửi request tới server
request.send();
Ở đoạn mã trên, bạn có thể thay đổi đường dẫn của API thành
“https://jsonplaceholder.typicode.com/todosabc/” để kiểm tra cho trường hợp không lấy được dữ liệu từ server về client.
Giờ chúng ta sẽ đưa cả đoạn mã xử lý ở trên vào một hàm, có
tên là getTodos()
const getTodos = () => {
const request = new XMLHttpRequest();
request.addEventListener('readystatechange', () => {
if(request.readyState === 4 && request.status === 200){
console.log(request.responseText);
} else if (request.readyState === 4 ){
console.log("Không nhận được dữ liệu từ server!");
}
});
request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
request.send();
};
getTodos();
Chúng ta sẽ tổ chức lại đoạn mã trên theo kiểu callback để tiện cho việc sử dụng về sau. Vậy là chúng ta sẽ sử dụng callback ở 2 nơi, một là trong bộ lắng nghe sự kiện addEventListener(), hai là trong hàm getTodos():
const getTodos = (callback) => {
const request = new XMLHttpRequest();
request.addEventListener('readystatechange', () => {
if(request.readyState === 4 && request.status === 200){
callback(undefined, request.responseText);
} else if (request.readyState === 4 ){
callback("Không nhận được dữ liệu từ server!", undefined);
}
});
request.open('GET', 'https://jsonplaceholder.typicode.com/todos/');
request.send();
};
getTodos( (err, data) => {
if(err){
console.log(err);
} else {
console.log(data);
}
});
Với hàm getTodos( (err,
data) => { }), bạn có thể tùy ý viết các xử lý khác nhau dựa trên dữ liệu
err và data nhận được.
Bạn có thể kiểm tra lại về tính bất đồng bộ của đoạn mã bằng
cách thêm vào các dòng lệnh khác như sau:
……
console.log("Câu lệnh 1");
console.log("Câu lệnh 2");
getTodos( (err, data) => {
if(err){
console.log(err);
} else {
console.log(data);
}
});
console.log("Câu lệnh 4");
// Câu lệnh 1
// Câu lệnh 2
// Câu lệnh 4
// dữ liệu của hàm callback getTodos
Chúng ta sẽ chuyển đổi dữ liệu do server gửi về thành đối tượng
(dùng phương thức JSON.parse()) để tiện cho việc xử lý sau này:
…
request.addEventListener('readystatechange', () => {
if(request.readyState === 4 && request.status === 200){
const data = JSON.parse(request.responseText);
callback(undefined, data);
} else if (request.readyState === 4 ){
callback("Không nhận được dữ liệu từ server!", undefined);
}
});
…
Tóm lại, để lập trình bất đồng bộ sử dụng kĩ thuật callback,
bạn cần thực hiện các việc sau:
– Bước 1: sử dụng kĩ thuật lập trình callback của JavaScript
– Bước 2: định nghĩa hàm, nhằm xử lý một tác vụ cụ thể,
trong đó có gọi tới hàm callback cho trường hợp xử lý thành công và xử lý thất
bại
– Bước 3: gọi hàm (đã định nghĩa ở Bước 2) để thực thi, trong
đó định nghĩa thêm các xử lý dựa trên dữ liệu trả về của hàm callback trong trường
hợp thành công và thất bại
Xem hình minh họa:
Hiện tượng callback hell
Callback hell là hiện tượng quá nhiều hàm callback lồng
nhau, làm cho mã nguồn khó đọc, khó bảo trì. Chữ “hell” có nghĩa là địa ngục, nỗi
ám ảnh; ý nói đoạn mã nguồn mà có nhiều hàm callback lồng nhau sẽ là nỗi ám ảnh
của lập trình viên.
Chúng ta cùng quay trở lại hàm getTodos() để lấy dữ liệu từ trên mạng. Giả sử trước khi lấy được dữ
liệu với id=3 từ trên mạng, bạn cần phải có dữ liệu với id = 1, và id = 2. Bạn
sẽ gọi 2 hàm callback lồng nhau với id = 1 và id = 2. Bạn sẽ sửa lại hàm getTodos()
để thực hiện yêu cầu này:
const getTodos = (resource, callback) => {
const request = new XMLHttpRequest();
request.addEventListener("readystatechange", ()=>{
if(request.readyState === 4 && request.status === 200) {
const data = JSON.parse(request.responseText);
callback(undefined, data);
} else if(request.readyState === 4){
callback("Không lấy được dữ liệu từ Server!", undefined);
}
});
request.open('GET', resource);
request.send();
}
// gọi hàm getTodos() để lấy dữ liệu, truyền theo
hàm callback (err, data) => {}
getTodos( "https://jsonplaceholder.typicode.com/todos/1", (err, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
// neu co du lieu id = 1 thi goi tiep
getTodos() voi callback (err, data) => {}
//
de xu ly du lieu id = 2
getTodos( "https://jsonplaceholder.typicode.com/todos/2", (err, data) => {
if(err) {
console.log(err);
} else {
console.log(data);
// neu co du lieu id = 2 thi goi tiep
getTodos() voi callback (err, data) => {}
//
de xu ly du lieu id = 3
getTodos( "https://jsonplaceholder.typicode.com/todos/3", (err, data) => {
if(err) {
console.log(err);
} else {
console.log(data);
}
});
}
});
}
});
// {userId: 1, id: 1, title: 'delectus aut autem', completed: false}
// {userId: 1, id: 2, title: 'quis ut nam facilis et officia qui',
completed: false}
// {userId: 1, id: 3, title: 'fugiat veniam minus', completed: false}
Ở đoạn mã trên, bạn sẽ thấy là khi nào có được dữ liệu với
id = 1, sẽ gọi tiếp hàm callback (err, data) => {}, khi có được dữ liệu với
id = 2, lại gọi tiếp hàm callback (err, data) => {}. Chỉ với 3 hàm callback
lồng nhau như vậy đã thấy đoạn mã nguồn rất phức tạp, khó hiểu, khó bảo trì.
Để khắc phục tình trạng callback hell, chúng ta sẽ tìm hiểu
một giải pháp cho lập trình bất đồng bộ khác có tên là promise.
-----
Cập nhật: 5/4/2022
-----