Promises là một chủ đề nóng trong lập trình JavaScript, và bạn chắc chắn nên làm quen với chúng.
Nhưng có vẻ lần đầu tiên không hề dễ dàng.
Có thể phải mất một vài lần nỗ lực, một vài ví dụ và phải thực hành kha khá để hiểu chúng.
Mục đích của mình với bài viết này là giúp bạn hiểu hơn về Promises trong JavaScript và thúc đẩy bạn thực hành, sử dụng promises nhiều hơn.
Mình sẽ giải thích kỹ càng Promises là gì, chúng giải quyết vấn đề gì và cách chúng hoạt động.
Mỗi phần được mô tả trong bài viết này sẽ đi kèm với một ví dụ để giúp bạn thực hành cùng và là cơ sở để bạn nắm giữ Promises.
> Trước khi tiếp tục, High-Recommend bạn đã học JavaScript căn bản vững chắc.
PROMISES LÀ GÌ?
Promises (lời hứa) là một lời hứa hẹn sẽ làm một cái gì đó. Nó có thể được coi như là bản sao không đồng bộ của hàm getter.
Bản chất của nó có thể được giải thích là:
promise.then(function(value) {
// Làm cái gì đó với 'value'
});
Promises có thể thay thế việc sử dụng không đồng bộ các lệnh callback và có một số ưu điểm hơn.
> Tham khảo: CALLBACK TRONG JAVASCRIPT
Ngày càng có nhiều thư viện và framework sử dụng chúng như một cách chính để xử lý không đồng bộ.
Trong đó, Ember.js là một ví dụ về một framework như vậy. Và cũng có một số thư viện triển khai đặc tả Promises / A +.
Chúng ta học cơ bản về Promises và tìm hiểu một vài ví dụ thực tế để giới thiệu các khái niệm đằng sau chúng.
Mình sẽ sử dụng một trong những thư viện triển khai phổ biến - rsvp.js trong các ví dụ.
Hãy bắt đầu nào!
SỬ DỤNG THƯ VIỆN RSVP.JS
Để cài đặt nó cho nodejs, hãy sử dụng command prompt và cd đến thư mục dự án của bạn và gõ lệnh sau:
npm install --save rsvp
Nếu bạn làm việc trên giao diện người dùng và sử dụng trình duyệt, thì đó chỉ là:
bower install -S rsvp
Hoặc nếu bạn chỉ muốn nhảy vào ngay lập tức, bạn có thể đưa nó vào thông qua thẻ <script></script>
trong thẻ <head>
, bạn chỉ cần thêm đoạn này.
<script src="https://cdn.jsdelivr.net/npm/rsvp@4/dist/rsvp.min.js"></script>
PROMISES CÓ NHỮNG TÍNH CHẤT NÀO?
Bạn cứ thử nghĩ về việc cho vay tiền thì biết lời hứa (Promises) nó như thế nào, con nợ hứa nhưng:
-
Chưa có tiền để trả - Chờ đi
Lập trình là một thứ rất thực tế. Thế nên, Promises cũng tương ứng như vậy, nó có thể ở một trong ba trạng thái:
Kể từ khi được tạo, Promises ở trạng thái pending. Từ đây, nó có thể chuyển sang trạng thái fulfilled hoặc bị rejected.
Chúng ta gọi sự chuyển đổi này là quyết định của Promises. Trạng thái được giải quyết của một Promise là trạng thái cuối cùng của nó.
Vì vậy một khi nó được thực hiện (fulfilled) hoặc bị từ chối (rejected), nó vẫn ở đó.
Cách tạo một Promises trong rsvp.js là thông qua cái được gọi là Revealing Constructor.
Loại phương thức khởi tạo này nhận một tham số duy nhất và ngay lập tức gọi nó với hai đối số, fulfill
và reject
, có thể chuyển đổi promise sang trạng thái fulfilled
hoặc rejected
.
var promise = new RSVP.Promise(function(fulfill, reject) {
(...)
});
JavaScript Promises Pattern này được gọi là Revealing Constructor vì đối số tiết lộ các khả năng của nó đối với constructor, nhưng đảm bảo rằng người dùng promises không thể chỉnh trạng thái của nó.
Người dùng Promises có thể phản ứng với những thay đổi trạng thái của nó bằng cách thêm trình xử lý thông qua phương thức then
.
Nó có một hàm thực hiện và một hàm từ chối, cả hai đều có thể là tùy chọn.
promise.then(onFulfilled, onRejected);
Tùy thuộc vào kết quả của quá trình giải quyết promises, trình xử lý onFulfilled
hoặc onRejected
được gọi là không đồng bộ (asynchronously).
Hãy xem một ví dụ tung xúc xắc để thấy mọi thứ được thực thi theo thứ tự nào:
function dieToss() {
return Math.floor(Math.random() * 6) + 1;
}
console.log('1');
var promise = new RSVP.Promise(function(fulfill, reject) {
var n = dieToss();
if (n === 6) {
fulfill(n);
} else {
reject(n);
}
console.log('2');
});
promise.then(function(toss) {
console.log('Yeah, nó ném ra ' + toss + '.');
}, function(toss) {
console.log('À không, nó ném ra ' + toss + '.');
});
console.log('3');
Đoạn code này sau đó cho ra kết quả tương tự như:
1
2
3
À không, nó ném ra 4.
Và nếu bạn may mắn, nó sẽ cho kết quả tương tự như thế này:
1
2
3
Yeah, nó ném ra 6.
Promises này thể hiện hai điều:
Thứ nhất, các trình xử lý mà chúng ta thêm vào promises đã thực sự được gọi sau khi tất cả các code khác chạy không đồng bộ.
Thứ hai, trình xử lý fulfill
chỉ được gọi khi promises đã được thực hiện, với giá trị mà nó đã được giải quyết (trong trường hợp của chúng ta là kết quả của việc tung xúc xắc).
Điều này cũng tương tự với trình xử lý reject
.
> 1S PR kiếm chút cháo: Nếu bạn đang tích cực học để trở lập trình web thì có thể tham khảo KHÓA HỌC JAVA hoặc KHÓA HỌC PHP tại NIIT - ICT Hà Nội nhé.
CHUỖI PROMISES
Đặc tả yêu cầu hàm then
(các trình xử lý) cũng phải trả về một promises, để cho phép xâu chuỗi các promise với nhau, dẫn đến code trông gần như đồng bộ:
signupPayingUser
.then(displayHoorayMessage)
.then(queueWelcomeEmail)
.then(queueHandwrittenPostcard)
.then(redirectToThankYouPage)
Ở đây, signupPayingUser
trả về một promise và mỗi hàm trong chuỗi promise sẽ được gọi với giá trị trả về của trình xử lý trước khi nó đã hoàn thành.
Đối với tất cả các mục đích thực tế, điều này tuần tự hóa các cuộc gọi mà không chặn chuỗi thực thi chính.
Để xem từng promise được giải quyết như thế nào với giá trị trả lại của item trước đó trong chuỗi.
Chúng ta quay lại với ví dụ tung xúc xắc. Chúng ta muốn tung xúc xắc tối đa 3 lần hoặc cho đến khi 6 điểm xuất hiện lần đầu tiên.
function tossASix() {
return new RSVP.Promise(function(fulfill, reject) {
var n = Math.floor(Math.random() * 6) + 1;
if (n === 6) {
fulfill(n);
} else {
reject(n);
}
});
}
function logAndTossAgain(toss) {
console.log("Ném ra " + toss + ", thử lại đi.");
return tossASix();
}
function logSuccess(toss) {
console.log("Yeah, đã ném ra " + toss);
}
function logFailure(toss) {
console.log("Ném ra " + toss + ". Quá tệ, không thể có 6 điểm");
}
tossASix()
.then(null, logAndTossAgain)
.then(null, logAndTossAgain)
.then(logSuccess, logFailure);
Khi bạn chạy đoạn ví dụ promises này, bạn sẽ thấy kết quả như thế này trong console:
Ném ra 2, thử lại đi.
Ném ra 1, thử lại đi.
Ném ra 4. Quá tệ, không thể có 6 điểm
Promise được trả về bởi tossASix
bị rejected khi lần tung không phải là số 6, do đó, trình xử lý reject được gọi với số điểm tung thực tế.
logAndTossAgain
in kết quả trên console và trả về một promise đại diện cho một lần tung xúc xắc khác.
Lần tung đó cũng bị từ chối và thực hiện logAndTossAgain
tiếp theo.
Tuy nhiên, đôi khi bạn số đỏ nên được 6 điểm.
Ném ra 4, thử lại đi.
Yeah, đã ném ra 6.
* Có khoảng 42% cơ hội để tung ra ít nhất một con sáu nếu bạn tung ba con xúc xắc.
Ví dụ trên còn cho chúng ta biết thêm vài thứ.
Làm thế nào để không tung thêm nữa sau lần tung đầu tiên và thành công?
Lưu ý rằng tất cả các trình xử lý fulfill (đối số đầu tiên trong các lệnh gọi đến then
) trong chuỗi là null
, ngoại trừ đối số của cái cuối cùng, logSuccess
.
Đặc tả yêu cầu rằng nếu một trình xử lý (fulfill hoặc reject) không phải là một hàm thì promise trả về phải được giải quyết với cùng một giá trị.
Trong ví dụ về các promise ở trên, trình xử lý fulfill đầu tiên, null
không phải là một hàm và giá trị của promise đã được thực hiện với một giá trị 6 (Giả sử)
Vì vậy, promise được trả về bởi lệnh gọi then
(lệnh tiếp theo trong chuỗi) cũng sẽ được thực hiện, với giá trị 6.
Điều này lặp lại cho đến khi gặp một trình xử lý fulfill là một hàm.
Do đó, quá trình thực hiện sẽ giảm dần cho đến khi nó được xử lý. Trong trường hợp của chúng ta, nó xảy ra ở cuối chuỗi.
XỬ LÝ LỖI
Đặc tả Promises / A + yêu cầu rằng nếu một promises bị rejected hoặc một lỗi được đưa ra trong trình xử lý recject, thì nó phải được xử lý bởi trình xử lý từ chối “downstream” từ nguồn.
Tận dụng kỹ thuật trickle down (nhỏ giọt) dưới đây để xử lý lỗi rõ ràng:
signupPayingUser
.then(displayHoorayMessage)
.then(queueWelcomeEmail)
.then(queueHandwrittenPostcard)
.then(redirectToThankYouPage)
.then(null, displayAndSendErrorReport)
Bởi vì một trình xử lý rejected chỉ được thêm vào cuối chuỗi, nếu bất kỳ trình xử lý hoàn thành nào trong chuỗi bị từ chối hoặc tạo ra lỗi, nó sẽ nhỏ giọt cho đến khi chạm vào displayAndSendErrorReport
.
Hãy quay trở lại với ví dụ tung xúc xắc của chúng ta và xem nó hoạt động như thế nào.
Giả sử chúng ta chỉ muốn ném xúc xắc không đồng thời và in ra kết quả:
var tossTable = {
1: "Một",
2: "Hai",
3: "Ba",
4: "Bốn",
5: "Năm",
6: "Sáu"
};
function toss() {
return new RSVP.Promise(function(fulfill, reject) {
var n = Math.floor(Math.random() * 6) + 1;
fulfill(n);
});
}
function logAndTossAgain(toss) {
var tossWord = tossTable[toss];
console.log("Ném ra " + tossWord.toUppercase() + " điểm.");
}
toss()
.then(logAndTossAgain)
.then(logAndTossAgain)
.then(logAndTossAgain);
Khi bạn chạy ví dụ trên, không có gì xảy ra. Không có gì được in trong console và dường như không có lỗi nào được đưa ra.
Trên thực tế, một lỗi vẫn xảy ra. Chúng ta không nhìn thấy lỗi đó vì không có trình xử lý từ reject nào trong chuỗi.
Vì code trong trình xử lý được thực thi không đồng bộ, với một stack mới, nó thậm chí không thoát khỏi console.
Hãy sửa lỗi này như sau:
var tossTable = {
1: "Một",
2: "Hai",
3: "Ba",
4: "Bốn",
5: "Năm",
6: "Sáu"
};
function toss() {
return new RSVP.Promise(function(fulfill, reject) {
var n = Math.floor(Math.random() * 6) + 1;
fulfill(n);
});
}
function logAndTossAgain(toss) {
var tossWord = tossTable[toss];
console.log("Ném ra " + tossWord.toUpperCase() + " điểm.");
}
function logErrorMessage(error) {
console.log("OMG: " + error.message);
}
toss()
.then(logAndTossAgain)
.then(logAndTossAgain)
.then(logAndTossAgain)
.then(null, logErrorMessage);
Chạy chương trình trên và chúng ta thấy gì đó tương tự như thế này:
"Ném ra NĂM điểm."
"OMG: Cannot read property 'toUpperCase' of undefined"
Chúng tôi đã quên return một cái gì đó từ logAndTossAgain
và promise thứ hai được thực hiện với undefined
.
Sau đó, trình xử lý fulfill tiếp theo sẽ lỗi khi cố gắng gọi toUpperCase
trên đó.
Đó là một điều quan trọng khác cần nhớ: Luôn return thứ gì đó từ những trình xử lý hoặc chuẩn bị cho những trình xử lý tiếp theo không có gì được truyền vào.
NÂNG CẤP PROMISES
Ở trên, chúng ta đã thấy những điều cơ bản về Promises trong JavaScript.
Một ưu điểm của việc sử dụng chúng là chúng có thể được cấu tạo theo những cách đơn giản để tạo ra những lời hứa “kết hợp” với hành vi mà chúng ta muốn.
Thư viện rsvp.js cung cấp một số ít trong số đó.
Đối với ví dụ cuối cùng, phức tạp nhất, chúng ta sẽ tung xúc xắc 3 lần để nhận được tổng điểm:
function toss() {
var n = Math.floor(Math.random() * 6) + 1;
return new RSVP.resolve(n); // [1]
}
function threeDice() {
var tosses = [];
function add(x, y) {
return x + y;
}
for (var i = 0; i < 3; i++) {
tosses.push(toss());
}
return RSVP.all(tosses).then(function(results) { // [2]
return results.reduce(add); // [3]
});
}
function logResults(result) {
console.log("Được " + result + " điểm với 3 xúc xắc.");
}
function logErrorMessage(error) {
console.log("OMG: " + error.message);
}
threeDice()
.then(logResults)
.then(null, logErrorMessage);
Chúng ta đã quen với toss
từ ví dụ trên.
Nó chỉ đơn giản tạo ra một promise luôn được thực hiện với kết quả của việc ném một con xúc xắc.
Mình đã sử dụng RSVP.resolve, một phương thức tạo ra một promise đơn giản hơn.
Trong threeDice
, mình đã tạo 3 promise mà mỗi promise đại diện cho một lần tung xúc xắc và cuối cùng kết hợp chúng với RSVP.all
.
RSVP.all
nhận một mảng các promise và được giải quyết bằng một mảng các giá trị đã phân giải, trong khi vẫn duy trì thứ tự của chúng.
Điều đó có nghĩa là chúng ta có kết quả của các lần tung (xem [2] trong đoạn code trên) và chúng ta trả về một promise được thực hiện với tổng của chúng (xem [3] trong đoạn code trên).
Cuối cùng, chúng ta có tổng:
"Được 11 điểm với 3 xúc xắc."
SỬ DỤNG PROMISE ĐỂ GIẢI QUYẾT CÁC VẤN ĐỀ THỰC TẾ
Promise của JavaScript được sử dụng để giải quyết các vấn đề trong các ứng dụng phức tạp hơn nhiều so với việc tung xúc xắc.
Nếu bạn thay thế việc tung 3 viên xúc xắc bằng việc gửi 3 ajax request đến endpoints riêng biệt và tiếp tục khi tất cả chúng đã trả về thành công (hoặc nếu bất kỳ điểm nào trong số chúng không thành công), bạn đã có một ứng dụng hữu ích của Promises và RSVP.all
.
Promises khi được sử dụng đúng cách, sẽ tạo ra code dễ đọc, logic hơn và do đó dễ debug hơn so với callback.
Chẳng hạn, bạn không cần thiết lập các quy ước liên quan đến xử lý lỗi vì chúng đã là một phần của đặc điểm kỹ thuật.
TẠM KẾT
Như vậy, qua bài viết này mình đã giúp bạn tìm hiểu một chút về Promises trong JavaScript.
Và nếu bạn chưa hiểu rõ điều gì về promises, hãy comment ở bên dưới để được giải đáp thêm nhé.
---
HỌC VIỆN ĐÀO TẠO CNTT NIIT - ICT HÀ NỘI
Học Lập trình chất lượng cao (Since 2002). Học làm Lập trình viên. Hành động ngay!
Đc: Tầng 3, 25T2, N05, Nguyễn Thị Thập, Cầu Giấy, Hà Nội
SĐT: 02435574074 - 0914939543
Email: hello@niithanoi.edu.vn
Fanpage: https://facebook.com/NIIT.ICT/
#niit #niithanoi #niiticthanoi #hoclaptrinh #khoahoclaptrinh #hoclaptrinhjava #hoclaptrinhphp #java #php #python