JavaScript: JavaScript Promises
Tổng quan
Trong hướng dẫn này, bạn sẽ tìm hiểu về Promise
trong JavaScript và cách sử dụng chúng một cách hiệu quả.
Ví dụ sau định nghĩa một hàm getUsers() trả về danh sách các đối tượng người dùng :
function getUsers() { return [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; }
Mỗi đối tượng người dùng có hai thuộc tính username
và email.
Để tìm người dùng theo tên người dùng từ danh sách người dùng được hàm getUsers()
trả về, bạn có thể sử dụng hàm findUser() như sau:
function findUser(username) { const users = getUsers(); const user = users.find((user) => user.username === username); return user; }
Trong hàm findUser()
:
- Đầu tiên, lấy mảng chứa các người dùng bằng cách gọi hàm getUsers()
- Thứ hai, tìm người dùng cụ thể theo
username
bằng cách sử dụng phương thức find() của đối tượng Array. - Thứ ba, trả lại người dùng phù hợp.
Phần sau đây hiển thị mã hoàn chỉnh để tìm người dùng có username
'john'
:
function getUsers() { return [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; } function findUser(username) { const users = getUsers(); const user = users.find((user) => user.username === username); return user; } console.log(findUser('john'))
Output:
{ username: 'john', email: 'john@test.com' }
Mã trong hàm findUser() là synchronous (đồng bộ) và block (bị chặn). Hàm findUser() thực thi hàm getUsers() để lấy một mảng người dùng, gọi phương thức find() trên mảng users để tìm kiếm người dùng có tên người dùng cụ thể và trả về người dùng phù hợp.
Trong thực tế, hàm getUsers() này có thể truy cập cơ sở dữ liệu hoặc gọi API để lấy danh sách người dùng. Vì vậy, hàm getUsers() sẽ có độ trễ. Để mô phỏng độ trễ, bạn có thể sử dụng hàm setTimeout(). Ví dụ:
function getUsers() { let users = []; // tạo độ trễ 1s (1000ms) setTimeout(() => { users = [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; }, 1000); return users; }
Làm thế nào nó hoạt động.
- Đầu tiên, xác định một mảng
users
và khởi tạo giá trị của nó bằng một mảng trống. - Thứ hai, gán một mảng người dùng cho biến users bên trong lệnh gọi lại của hàm setTimeout().
- Thứ ba, trả về mảng users
Hàm getUsers()
sẽ không hoạt động bình thường và luôn trả về một mảng trống. Do đó, hàm findUser()
sẽ không hoạt động như mong đợi:
function getUsers() { let users = []; setTimeout(() => { users = [ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]; }, 1000); return users; } function findUser(username) { const users = getUsers(); // A const user = users.find((user) => user.username === username); // B return user; } console.log(findUser('john')) // undefined
Vì getUsers()
trả về một mảng trống nên mảng users trống (dòng A). Khi gọi phương thức find() trên mảng users, phương thức trả về undefined
(dòng B)
Thách thức là làm thế nào để truy cập vào kết quả users được trả về từ getUsers()
sau một giây. Một cách tiếp cận cổ điển là sử dụng hàm gọi lại (callback function).
Sử dụng hàm gọi lại để xử lý hoạt động không đồng bộ
Ví dụ sau đây thêm đối số gọi lại vào hàm getUsers()
và findUser()
:
function getUsers(callback) { setTimeout(() => { callback([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); }, 1000); } function findUser(username, callback) { getUsers((users) => { const user = users.find((user) => user.username === username); callback(user); }); } findUser('john', console.log);
Output
{ username: 'john', email: 'john@test.com' }
Trong ví dụ này, hàm getUsers() chấp nhận hàm gọi lại làm đối số và gọi hàm đó bằng mảng users bên trong setTimeout()
. Ngoài ra, findUser()
chấp nhận hàm gọi lại để xử lý người dùng phù hợp.
Cách tiếp cận này hoạt động rất tốt. Tuy nhiên, nó làm cho mã khó theo dõi hơn. Ngoài ra, nó còn tăng thêm độ phức tạp cho các hàm có đối số gọi lại.
Nếu số lượng hàm tăng lên, bạn có thể gặp phải vấn đề về cuộc gọi lại. Để giải quyết vấn đề này, JavaScript đưa ra khái niệm về Promise.
JavaScript Promises
Theo định nghĩa, một Promise là một đối tượng gói gọn kết quả của một hoạt động không đồng bộ (asynchronous operation)
Một đối tượng Promise có trạng thái có thể là một trong những trạng thái sau:
- Chưa giải quyết (pending)
- Hoàn thành (Fulfilled) với một giá trị
- Bị từ chối (Rejected ) vì một lý do
Lúc đầu, trạng thái của một Promise là đang chờ xử lý hay chưa giải quyết (pending), cho biết rằng hoạt động không đồng bộ đang được tiến hành. Tùy thuộc vào kết quả của hoạt động không đồng bộ, trạng thái thay đổi thành hoàn thành hoặc bị từ chối.
Trạng thái hoàn thành cho biết hoạt động không đồng bộ đã được hoàn thành thành công:
Trạng thái bị từ chối cho biết hoạt động không đồng bộ không thành công.
Tạo một Promise
Để tạo một đối tượng Promise, bạn sử dụng hàm tạo Promise():
const promise = new Promise((resolve, reject) => { // contain an operation // ... // return the state if (success) { resolve(value); } else { reject(error); } });
Hàm tạo Promise chấp nhận hàm gọi lại thường thực hiện thao tác không đồng bộ. Chức năng này thường được gọi là executor (người thực thi).
Đổi lại, executor chấp nhận hai hàm gọi lại có tên resolve
và reject
Lưu ý rằng các hàm gọi lại được truyền vào bộ thực thi resolve
và reject
theo quy ước.
Nếu thao tác không đồng bộ hoàn tất thành công, executor sẽ gọi hàm resolve()
để thay đổi trạng thái của Promise từ đang chờ xử lý sang hoàn thành đi kèm với một giá trị.
Trong trường hợp có lỗi, executor sẽ gọi hàm reject() để thay đổi trạng thái của Promise từ đang chờ xử lý sang bị từ chối kèm theo lý do lỗi.
Khi một Promise đạt đến trạng thái được thực hiện hoặc bị từ chối, nó sẽ vẫn ở trạng thái đó và không thể chuyển sang trạng thái khác.
Nói cách khác, một Promise không thể đi từ trạng thái fulfilled
này sang trạng thái rejected khác và ngược lại. Ngoài ra, nó không thể quay trở lại từ trạng thái fulfilled
hoặc rejected sang
pending
.
Khi một đối tượng Promise mới được tạo, trạng thái của nó là đang chờ xử lý. Nếu một Promise đạt được trạng thái fulfilled
hoặc rejected
nghĩa là nó sẽ được giải quyết (resolved).
Lưu ý rằng bạn sẽ hiếm khi tạo các đối tượng Promise trong thực tế. Thay vào đó, bạn sẽ sử dụng những Promise do thư viện cung cấp.
Sử dụng Promise với then, catch, finally
1) Phương thức then()
Để nhận được giá trị của một Promise khi nó được thực hiện, bạn gọi phương thức then() của đối tượng Promise. Đoạn mã sau đây cho thấy cú pháp của phương thức then():
promise.then(onFulfilled,onRejected);
Phương thức then() này chấp nhận hai hàm gọi lại: onFulfilled
và onRejected
.
Phương thức then()
gọi onFulfilled()
với một giá trị nếu Promise được thực hiện hoặc gọi onRejected()
với một lỗi nếu Promise bị từ chối.
Lưu ý rằng cả 2 đối số đối số onFulfilled
và onRejected
đều là tùy chọn.
Ví dụ sau đây cho thấy cách sử dụng phương thức then() của đối tượng Promise được trả về từ hàm getUsers()
:
function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); }, 1000); }); } function onFulfilled(users) { console.log(users); } const promise = getUsers(); promise.then(onFulfilled);
Output
[ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' } ]
Trong ví dụ này:
- Đầu tiên, xác định hàm onFulfilled() sẽ được gọi khi Promise được thực hiện.
- Thứ hai, gọi
getUsers()
để lấy đối tượng Promise. - Thứ ba, gọi phương thức then() của đối tượng Promise và xuất danh sách người dùng ra console.
Để làm cho mã ngắn gọn hơn, bạn có thể sử dụng hàm mũi tên làm đối số của phương thức then() như sau:
function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); }, 1000); }); } const promise = getUsers(); promise.then((users) => { console.log(users); })
Vì hàm getUsers() trả về một đối tượng Promise nên bạn có thể xâu chuỗi lệnh gọi hàm bằng phương thức then() như sau:
// getUsers() function //... getUsers().then((users) => { console.log(users); });
Trong ví dụ này, hàm getUsers() luôn thành công. Để mô phỏng lỗi, chúng ta có thể sử dụng flag success
như sau:
let success = true; function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { if (success) { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); } else { reject('Failed to the user list'); } }, 1000); }); } function onFulfilled(users) { console.log(users); } function onRejected(error) { console.log(error); } const promise = getUsers(); promise.then(onFulfilled, onRejected);
2) Phương thức catch()
Nếu bạn chỉ muốn nhận lỗi khi trạng thái của Promise bị từ chối, bạn có thể sử dụng phương thức catch() của đối tượng Promise:
promise.catch(onRejected);
Bên trong, phương thức catch() này gọi phương thức then(undefined, onRejected).
Ví dụ sau thay đổi flag success
thành false
để mô phỏng tình huống lỗi:
let success = false; function getUsers() { return new Promise((resolve, reject) => { setTimeout(() => { if (success) { resolve([ { username: 'john', email: 'john@test.com' }, { username: 'jane', email: 'jane@test.com' }, ]); } else { reject('Failed to the user list'); } }, 1000); }); } const promise = getUsers(); promise.catch((error) => { console.log(error); });
3) Phương thức finally()
Đôi khi, bạn muốn thực thi cùng một đoạn mã cho dù Promise được thực hiện thành công hay bị từ chối. Ví dụ:
const render = () => { //... }; getUsers() .then((users) => { console.log(users); render(); }) .catch((error) => { console.log(error); render(); });
Như bạn có thể thấy, lệnh gọi hàm render() được sao chép trong cả hai phương thức then()
và catch()
.
Để loại bỏ việc lặp lại mã này, bạn sử dụng phương thức finally() như sau:
const render = () => { //... }; getUsers() .then((users) => { console.log(users); }) .catch((error) => { console.log(error); }) .finally(() => { render(); })
Một ví dụ về Promise JavaScript thực tế
Giả sử bạn có tệp JSON thông qua một url và có nội dung sau:
{ "message": "JavaScript Promise Demo" }
Phần sau đây hiển thị trang HTML có chứa một nút. Khi bạn nhấp vào nút, trang sẽ tải dữ liệu từ tệp JSON và hiển thị thông báo:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>JavaScript Promise Demo</title> <link href="css/style.css" rel="stylesheet"> </head> <body> <div id="container"> <div id="message"></div> <button id="btnGet">Get Message</button> </div> <script src="js/promise-demo.js"> </script> </body> </html>
Phần sau đây hiển thị tệp Promise-demo.js:
function load(url) { return new Promise(function (resolve, reject) { const request = new XMLHttpRequest(); request.onreadystatechange = function () { if (this.readyState === 4 && this.status == 200) { resolve(this.response); } else { reject(this.status); } }; request.open('GET', url, true); request.send(); }); } const url = 'https://www.javascripttutorial.net/sample/promise/api.json'; const btn = document.querySelector('#btnGet'); const msg = document.querySelector('#message'); btn.addEventListener('click', () => { load(URL) .then((response) => { const result = JSON.parse(response); msg.innerHTML = result.message; }) .catch((error) => { msg.innerHTML = `Error getting the message, HTTP status: ${error}`; }); })
Làm thế nào nó hoạt động.
Đầu tiên, xác định hàm load() sử dụng đối tượng XMLHttpRequest để tải tệp JSON từ máy chủ:
function load(url) { return new Promise(function (resolve, reject) { const request = new XMLHttpRequest(); request.onreadystatechange = function () { if (this.readyState === 4 && this.status == 200) { resolve(this.response); } else { reject(this.status); } }; request.open('GET', url, true); request.send(); }); }
Trong trình thực thi, chúng tôi gọi hàm resolve() có Response (phản hồi) nếu mã trạng thái HTTP là 200. Nếu không, chúng tôi gọi hàm reject()
có mã trạng thái HTTP.
Thứ hai, đăng ký trình xử lý sự kiện nhấn nút và gọi phương thức then() của đối tượng Promise. Nếu tải thành công, chúng tôi sẽ hiển thị thông báo được máy chủ trả về. Nếu không, chúng tôi sẽ hiển thị thông báo lỗi kèm theo mã trạng thái HTTP.
const url = 'https://www.javascripttutorial.net/sample/promise/api.json'; const btn = document.querySelector('#btnGet'); const msg = document.querySelector('#message'); btn.addEventListener('click', () => { load(URL) .then((response) => { const result = JSON.parse(response); msg.innerHTML = result.message; }) .catch((error) => { msg.innerHTML = `Error getting the message, HTTP status: ${error}`; }); })