Promise pattern – Cách thức giúp JS dev tránh nested callbacks

Bài viết sau được mình dịch từ một bài viết trên Tutplus.com, xin mời các bạn cùng xem qua và cho ý kiến nhé.
Link gốc bài viết : http://code.tutsplus.com/vi/tutorials/keeping-promises-with-javascript–cms-25056?ec_unit=dropdown-language

Javascript, với sự phổ biến và những phát triển gần đây, đang nhanh chóng trở thành bạn thân của giới lập trình web. Đúng nghĩa “bạn thân”, Javascript giữ đúng những lời hứa (Promise) của nó.

Nghe có vẻ hơi kỳ lạ, nhưng đó là sự thật. Hầu hết các trình duyệt hiện nay đều đang hỗ trợ cái gọi là đối tượng Promise này. Một promise là một chức năng ở đó mô tả một đoạn code hay task mà bạn muốn sẽ được thực thi sau này.

Một promise sẽ trông như thế này:

var myPromise = new Promise(function (resolve, reject) {
 // Task to carry out goes here.
});

Bạn có thể thấy khi ta tạo một promise đưa nó một tham số làm chức năng chứa đoạn code chúng ta muốn thực thi trong tương lai. Có thể bạn cũng để ý đến hai tham số truyền qua cho promise, resolved và reject Chúng cũng là những chức năng và là cách mà chúng ta gọi Promise thực hiện cái mà nó đã hứa. Giờ là cách mà ta sử dụng chúng:

var myPromise = new Promise(function (resolve, reject) {
 if (true) {
   resolve('Hello Tuts+ fans!');
 } else {
   reject('Aww, didn\'t work.');
 }
});

Rõ ràng, promise này sẽ gọi resolve bởi về câu lệnh if luôn trả giá trị true. Ví dụ này chỉ giúp bạn nhận biết vấn đề – chúng ta sẽ có những ví dụ thực tế hơn – nhưng hãy tưởng tượng nếu thay thế true bằng một đoạn code bạn không chắc 100% nó hoạt động.

Giờ bắt đầu ta tạo một promise, làm sao sử dụng ? Ta cần gọi nó là function resolve và reject. và thực hiện bằng phương thức then của promise này.

myPromise.then(function (result) {
   // Resolve callback.
   console.log(result); 
}, function (result) {
   // Reject callback.
   console.error(result);
});

Lý do là mệnh đề if luôn trả về true, nên đoạn code trên sẽ luôn ghi log “Hello Tuts+ fans!” vào console. Nó cũng sẽ thực hiện ngay lập tức. Bởi vì, đoạn code bên trong phương thức khởi tạo của Promise chạy đồng bộ (synchronous), nó sẽ thực thi mà không đợi bất cứ thao tác nào khác. Nó có đầy đủ thông tin cần thiết để tiếp tục.

Chỗ mà promise thực sự nổi trội, là khi nó chạy dưới dạng những task bất đồng bộ (asynchronous) – đây là cái mà bạn không biết chính xác promise hoạt động như thế nào. Một ví dụ thực tế về task bất đồng bộ là kéo tài nguyên về, ví dụ như một file JSON, thông qua AJAX. Ta không chắc server sẽ tốn bao lâu để trả kết quả, hay có thể là gặp lỗi. Giờ thêm AJAX vào đoạn code promise của chúng ta.

var myPromise = new Promise(function (resolve, reject) {
 // Standard AJAX request setup and load.
 var request = new XMLHttpRequest();
 
 // Request a user's comment from our fake blog.
 request.open('GET', 'http://jsonplaceholder.typicode.com/posts/1');
 
 // Set function to call when resource is loaded.
 request.onload = function () {
 if (request.status === 200) {
   resolve(request.response);
 } else {
   reject('Page loaded, but status not OK.');
 }
 };
 
 // Set function to call when loading fails.
 request.onerror = function () {
   reject('Aww, didn\'t work at all.');
 }
 
 request.send();
});

Đoạn code này là Javascript chuẩn dùng để gọi AJAX. Ta gửi yêu cầu để request một tài nguyên từ URL định sẵn, ở đây là một file JSON, và chờ kết quả. Ta sẽ không biết chắc khi nào nó trả về. Rõ ràng ta không muốn tạm dừng và chờ đợi một script bên ngoài, vậy thì ta phải làm gì đây?

Và may mắn là ta đã bỏ đoạn code này bên trong promise. Bằng cách này, cơ bản ta thông báo rằng “Này đoạn code kia, tôi phải đi tiếp nhưng tôi sẽ gọi lại sau cho bạn và thông báo khi nào cần thực thi. Phải đảm bảo bạn sẽ thực hiện và nói tôi biết khi nào bạn xong nhé?” Và đoạn code này sẽ nói “Vâng, dĩ nhiên. Tôi hứa.”

Điều quan trọng lưu ý trong đoạn code trên là việc gọi đến 2 hàm resolve và reject. Nhớ là, có những promise mà ta gọi có thể dẫn đến đoạn code chạy hoặc không. Hoặc là, ta sẽ không bao giờ biết.

Dùng đoạn code sau cho ví dụ cơ bản của chúng ta, ta có thể thấy cách gọi AJAX bên trong một promise như thế nào

// Tell our promise to execute its code
// and tell us when it's done.
myPromise.then(function (result) {
 // Prints received JSON to the console.
 console.log(result);
}, function (result) {
 // Prints "Aww didn't work" or
 // "Page loaded, but status not OK."
 console.error(result); 
});

Tôi biết rằng chúng tôi tin bạn, myPromise.

Chuỗi các Promise.

Giờ có thể bạn nghĩ rằng các promise chỉ là những hàm callback nhất thời với một syntax đẹp đẽ hơn. Nó đúng ở một mức độ nào đó, những hãy tiếp tục với ví dụ AJAX của chúng ta, bạn cần gọi thêm vài request, mỗi request dựa trên kết quả trước đó của nó. Hay nếu cái bạn cần là xử lý JSON trước ?

Thực hiện điều này bằng các callback dẫn bạn đến ngõ cụt với những hàm lồng xếp vào nhau, nó sẽ tăng sự phức tạp lên đáng kể cho việc theo dõi luồng code. May mắn là chúng ta có thể móc nối những hàm với nhau trong thế giới những promise. Đây là một ví dụ cách đế mỗi khi nhận JSON từ comment của user trên cái blog giả của chúng ta, ta sẽ muốn đảm bào tất cả chúng phải được viết thường trước khi thực hiện thêm bước nào khác.

myPromise
 .then(function (result) {
 // Once we receive JSON,
 // turn it into a JSON object and return.
 return JSON.parse(result);
 })
 .then(function (parsedJSON) {
 // Once json has been parsed,
 // get the email address and make it lowercase.
 return parsedJSON.email.toLowerCase();
 })
 .then(function (emailAddress) {
 // Once text has been made lowercase,
 // print it to the console.
 console.log(emailAddress);
 }, function (err) {
 // Something in the above chain went wrong?
 // Print reject output.
 console.error(err);
 });

Ở đây bạn có thể thấy trong khi ban đầu gọi bất đồng bộ, nó vẫn có thể móc nối bằng chuỗi gọi đồng bộ. Đoạn code trong mỗi hàm resolve bên trong mệnh đề then sẽ được gọi mỗi lần return. Bạn cũng lưu ý là chỉ có một hàm error được mô tả trong chuỗi này. Cách đăt hàm reject ở hàm then cuối cùng chuỗi sẽ giúp bất kỳ promise nào gọi reject sẽ gọi đến nó.

Bây giờ chúng ta thấy tự tin hơn với các promise, hãy tạo một hàm khác liên kết với cái ở trên. Ta sẽ tạo một cái lấy địa chỉ email có chứa kí tự thường và sẽ gửi một email đến địa chỉ này. Đây chỉ là một ví dụ mô tả cho xử lý bất đồng bộ – nó có thể là bất cứ thứ gì, như contact đến server để xem email có nằm trong whitelist hay không hoặc user đã đăng nhập hay chưa. Ta sẽ cần đưa email đến một cái promise mới, nhưng promise không nhận tham số đưa vào. Cách này để quản lý promise trong hàm của nó, như sau:

var sendEmail = function (emailAddress) {
 return new Promise(function (resolve, reject) {
 // Pretend to send an email
 // or do something else asynchronous
 setTimeout(function () {
 resolve('Email sent to ' + emailAddress);
 }, 3000);
 });
};

Ta đang dùng setTimeout ở đây để đơn giản làm giả một task chạy bất đồng bộ kéo dài vài giây.

Và giờ làm sao để dùng hàm tạo ra promise mới? Nếu mỗi hàm resolve được dùng trong mệnh đề then đều trả về một function thì ta có thể dùng nó theo cách tương tự với hàm đồng bộ thông thường.

myPromise
 .then(function (result) {
 return JSON.parse(result);
 })
 .then(function (parsedJSON) {
 return parsedJSON.email.toLowerCase();
 })
 .then(function (emailAddress) {
 return sendEmail(emailAddress)
 })
 .then(function (result) {
 // Outputs "Email sent to stuart@fakemail.biz"
 console.log(result);
 }, function (err) {
 console.error(err);
 });

Giờ hãy nhìn lại flow này và tổng hợp những gì đang diễn ra. Hứa hẹn myPromise của chúng ta ban đầu yêu cầu một đoạn JSON. Khi JSON nhận về (chúng ta không biến khi nào), ta sẽ bỏ JSON vào một đối đượng JavaScript và trả ra giá trị.

Khi nào xong, ta lấy email trong JSON và biến thành ký tự thường. Sau đó ta gửi email đến địa chỉ đó, và một lần nữa không biết khi nào sẽ hoàn thành, nhưng khi nào xong ta sẽ in console ra một message là thành công. Và nhìn không có sự lồng ghép nào ở đây.

Kết luận

Tôi hy vọng bài viết này đã giới thiệu những Promise có ích và cho bạn những lý do để bắt đầu những dự án JavaScript của mình. Nếu bạn muốn học thêm về Promise thì có thể xem thêm bài viết HTML5 Rocks của Jake Archibald liên quan đến chủ đề này.

Leave a Reply

Related Post