Bài trước: Web nâng cao (8) - JavaScript cho React (7) - Bất đồng bộ bằng callback
-----1.1.1
Lập trình bất đồng bộ bằng promise
Promise là một cơ chế (hay một kĩ thuật lập trình) của
JavaScript, nó giúp bạn lập trình bất đồng bộ mà không rơi vào tình trạng
callback hell.
Vì promise cũng là một kĩ thuật lập trình bất đồng bộ,
nên để dễ hiểu về promise, bạn cần nhìn lại một chút về lập trình bất đồng bộ bằng
callback. Trong lập trình bất đồng bộ bằng kĩ thuật callback, bạn sẽ định nghĩa
một hàm xử lý nghiệp vụ bất kì (ví dụ lấy dữ liệu từ trên mạng về), bạn sẽ truyền
vào hàm xử lý nghiệp vụ một hàm callback và sẽ gọi hàm callback này ở thời điểm
thích hợp, ứng với việc lấy dữ liệu thành công hay thất bại; khi gọi hàm xử lý
nghiệp vụ, bạn sẽ viết thêm phần xử lý cho giá trị trả về của hàm callback ứng
với trường hợp thành công và thất bại.
Để hiểu về kĩ thuật lập trình bất đồng bộ bằng promise, bạn
cũng sẽ tiếp cận theo cách sau:
– Hiểu được promise là gì, nó chạy như thế nào
– Định nghĩa hàm xử lý một nghiệp vụ nào đó có dùng tới
promise
– Gọi hàm xử lý nghiệp vụ (vừa định nghĩa ở bước trên),
Quan sát ví dụ để hiểu kĩ thuật lập trình promise
Promise nghĩa là một “lời hứa”, do vậy sẽ có quá
trình tạo ra lời hứa, quá trình thực hiện lời hứa (có thể thực hiện được hoặc
không).
JavaScript hiện thực ý tưởng “lời hứa” vào trong đối tượng
promise. Vì là đối tượng, nên promise sẽ có các thuộc tính và phương thức cần
thiết để nó có thể hoạt động được.
Lập trình bất đồng bộ bằng promise thực chất là việc nhúng
(hay gắn) một đối tượng promise vào một hàm xử lý nghiệp vụ bất kì. Khi đã được
nhúng promise, chúng ta có thể định nghĩa hàm xử lý nghiệp vụ, gọi hàm xử lý
nghiệp vụ dựa trên cơ chế hoạt động của promise, giúp cho việc xử lý nghiệp vụ
được thực thi theo cơ chế bất đồng bộ.
Ví dụ sau sẽ gắn đối tượng promise vào hàm layDuLieu():
const layDuLieu = () => {
return new Promise( (resolve, reject) => {
});
};
Ở đoạn mã trên, bạn đã khởi tạo một đối tượng promise (dùng
từ khóa new), thực chất promise cũng
là một hàm (function), rồi gán hàm promise vào hàm layDuLieu().
Một promise, hay một hàm có nhúng promise khi mới được tạo
ra (mới khai báo) sẽ ở trạng thái chờ (pending). Ví dụ:
const layDuLieu = () => {
return new Promise( (resolve, reject) => {
});
};
console.log(layDuLieu());
// Promise {<pending>}
Khi khởi tạo đối tượng promise, bạn sẽ truyền vào cho nó một
hàm callback gồm 2 tham số (resolve, reject) => {}; trong hàm callback này bạn
sẽ viết mã nguồn xử lý nghiệp vụ.
Bản thân tham số resolve và reject lại là hai hàm, dùng để xử
lý cho hai tình huống: “lời hứa thực hiện thành công” (resolve) và “không thực
hiện được lời hứa” (reject). Vì vậy, về bản chất kĩ thuật promise cũng vẫn dựa
trên kĩ thuật callback, chỉ khác nhau ở cách thức thực hiện.
Hàm resolve sẽ được gọi khi “lời hứa thực hiện thành công”
hay nghiệp vụ xử lý thành công. Hàm reject sẽ được gọi khi “lời hứa không thực
hiện được” hay nghiệp vụ xử lý thất bại.
Khi gọi hàm xử lý nghiệp vụ, kết quả trả về của promise sẽ
được gửi tới phương thức .then() hoặc .catch(). Bạn sẽ viết các hàm để xử lý
tùy theo kết quả trả về của promise.
Ví dụ sau là trường hợp xử lý nghiệp vụ thành công
(lấy được dữ liệu), khi đó hàm resolve() được gọi để trả về dữ liệu lấy được.
Khi gọi hàm layDuLieu(), trong tham số
đầu tiên của phương thức then(), bạn
sẽ viết hàm callback để xử lý dữ liệu nhận được, tham số của hàm callback (ví dụ
data) sẽ chứa dữ liệu do hàm
resolve() trả về:
const layDuLieu = () => {
return new Promise( (resolve, reject) => {
// các xử lý để lấy dữ liệu
resolve("Du lieu");
});
};
layDuLieu().then( (data) => {
console.log(data);
});
Ví dụ sau là trường hợp xử lý nghiệp vụ thất bại
(không lấy được dữ liệu), khi đó hàm reject() được gọi để trả về thông báo lỗi.
Khi gọi hàm layDuLieu(), trong tham số
thứ 2 của phương thức then(), bạn sẽ
viết hàm callback để xử lý lỗi, tham số của hàm callback (ví
dụ err) sẽ chứa thông báo lỗi do hàm
reject() trả về:
const layDuLieu = () => {
return new Promise( (resolve, reject) => {
// các xử lý để lấy dữ liệu
reject("khong
lay duoc du lieu");
});
};
layDuLieu().then( (data) => {
console.log(data);
}, (err) => {
console.log(err);
});
Do các hàm callback trong phương thức then() chỉ có một tham số
nên bạn có thể bỏ với dấu ngoặc đơn cho gọn:
layDuLieu().then(data => {
console.log(data);
}, err => {
console.log(err);
});
Bạn cũng có thể đưa hàm callback thứ 2 của phương thức
then() vào phương thức catch() như sau:
layDuLieu().then(data => {
console.log(data);
})
.catch(err => {
console.log(err);
});
Áp dụng promise vào thực tế
Tới đây, bạn đã hiểu kĩ thuật lập trình bất đồng bộ bằng
promise, giờ bạn sẽ áp dụng promise vào một ví dụ thực tế.
Giả sử bạn cần viết một hàm, có tên là getTodos(), để lấy dữ
liệu từ một server, có URL là https://jsonplaceholder.typicode.com/todos/1 :
// định nghĩa hàm getTodos, có nhúng đối tượng promise
const getTodos = (resource) => {
return new Promise( (resolve, reject) => {
const request = new XMLHttpRequest();
request.addEventListener("readystatechange", () => {
if(request.readyState === 4 && request.status === 200) {
const data = JSON.parse(request.responseText);
resolve(data);
} else if(request.readyState === 4) {
reject("Không lấy được dữ liệu từ Server!")
}
});
request.open('GET', resource);
request.send();
});
};
// gọi để thực thi hàm getTodos(),
// sử dụng cơ chế trả dữ liệu về của promise để xử lý
getTodos("https://jsonplaceholder.typicode.com/todos/1").then( (data) => {
console.log(data);
})
.catch( (err) => {
console.log(err);
});
Bạn có thể thay đổi URL của server để hàm getTodos() không
nhận được dữ liệu: ví dụ https://jsonplaceholder.typicode.com/todosabc/1.
Chuỗi các promise (chaining promises)
Như đã trình bày, promise sẽ giúp bạn tránh được tình trạng
callback hell (quá nhiều hàm callback lồng nhau). Làm được điều này là do sau
khi phương thức then() thực thi xong, bạn có thể trả về một promise khác, do vậy
có thể gọi tiếp các phương thức then(). Tính chất này được gọi là chuỗi các
promise hay chaining promises.
Quay trở lại ví dụ đã thực hiện ở phần callback hell: dùng hàm
getTodos() để lấy dữ liệu từ server.
Giả sử trước khi lấy được dữ liệu với id=3, bạn cần phải có dữ liệu
với id = 1, và id = 2. Như vậy sẽ có tình trạng 3 hàm callback lồng nhau. Xem
đoạn mã nguồn minh họa.
getTodos( "https://jsonplaceholder.typicode.com/todos/1", (err, data) => {
if (err) {
console.log(err);
} else {
console.log(data);
getTodos( "https://jsonplaceholder.typicode.com/todos/2", (err, data) => {
if(err) {
console.log(err);
} else {
console.log(data);
getTodos( "https://jsonplaceholder.typicode.com/todos/3", (err, data) => {
if(err) {
console.log(err);
} else {
console.log(data);
}
});
}
});
}
});
Với chaining promises, bạn có thể viết lại đoạn mã lấy dữ liệu
như sau:
<script>
// định nghĩa hàm getTodos, có nhúng đối tượng promise
const getTodos = (resource) => {
return new Promise( (resolve, reject) => {
const request = new XMLHttpRequest();
request.addEventListener("readystatechange", () => {
if(request.readyState === 4 && request.status === 200) {
const data = JSON.parse(request.responseText);
resolve(data);
} else if(request.readyState === 4) {
reject("Không lấy được dữ liệu từ Server!")
}
});
request.open('GET', resource);
request.send();
});
};
// gọi để thực thi hàm getTodos(),
// sử dụng cơ chế trả dữ liệu về của promise để xử lý
getTodos("https://jsonplaceholder.typicode.com/todos/1").then( (data) => {
console.log("promise 1: ", data);
return getTodos("https://jsonplaceholder.typicode.com/todos/2");
}).then( (data) => {
console.log("promise 2: ", data);
return getTodos("https://jsonplaceholder.typicode.com/todos/3");
}).then( (data) => {
console.log("promise 3:", data);
})
.catch( (err) => {
console.log(err);
});
</script>
Lưu ý, bất cứ đối tượng promise nào trả về lỗi thì lỗi đó sẽ
được chuyển tới phương thức .catch() để xử lý, và ngưng quá trình xử lý các
promise tiếp theo.
Cập nhật: 19/4/2022
-----