Xin chào, nếu đã từng lập trình với Javascript, hẳn bạn đã có đôi lần nghe nói / sử dụng callback. Và với sự phát triển như hiện nay của Javascript, thì có một vấn đề cực kỳ nhức nhối đã được thể hiện với callback của Javascript, đó là callback hell.
Dưới đây là ví dụ (yaoming)

callbackhell

Và để giải quyết vấn đề này, có một lời hứa đã được hiện thực hóa, vâng tên của nó chính là Promise. Trong bài dịch lần này từ SitePoint (https://www.sitepoint.com/overview-javascript-promises/), mình sẽ giới thiệu Promise và tác dụng của nó trong việc giải quyết đống trên kia :D

Đây thực sự là một tin tốt lành, giống như là quà Giáng Sinh dành cho các Javascript Developers vậy. Bạn sẽ cảm thấy thật hạnh phúc khi biết rằng Promise giờ đã trở thành một trong những thư viện chuẩn của JavaScript. Chrome 32 bản beta đã implement các API Promise basic. Tư tưởng của Promise không hề mới với developer. Hầu hết chúng ta ddax từng dùng Promise trong một vài thư viện đã có của JS như là Q, when, RSVP, Bluebird,... Ngay cả jQuery cũng có một chức năng gọi là Deferred Object hoạt động tương tự như Promise. Nhưng việc Promise được support native thật đáng kinh ngạc. Và bài viết dưới đây sẽ miêu tả những điểm cơ bản của Promise và cách để đưa code JS của bạn lên một tầm cao mới :D

Overview

Một object Promise đại diện cho một giá trị ở thời điểm hiện tại có thể chưa tồn tại, nhưng sẽ được xử lý và có giá trị vào một thời gian nào đó trong tương lai. Việc này sẽ giúp bạn viết các dòng code xử lý không đồng bộ trông có vẻ đồng bộ hơn. Lấy ví dụ, nếu bạn sử dụng Promise cho việc gọi API để lấy dữ liệu, bạn sẽ tạo ra một đối tượng Promise đại diện cho data lấy được từ API. Điều cốt lõi ở đây là dữ liệu sẽ chưa tồn tại ở thời điểm đối tượng Promise được tạo ra, mà chỉ có thể truy cập sau khi có response từ web service. Trong thời gian chờ lấy dữ liệu, Promise object sẽ đóng vai trò như một proxy cho dữ liệu. Hơn nữa, bạn có thể đính các callback vào Promise object để thực hiện việc xử lý dữ liệu. Các callback này sẽ chỉ thực hiện khi dữ liệu đã sẵn sàng.

Promise API

Tiếp theo đây chúng ta sẽ đến với các API cơ bản của Promise
Để bắt đầu, hãy cùng tìm hiểu đoạn code sau, đây là cách khởi tạo một đối tượng Promise .

if (window.Promise) { // Check if the browser supports Promises
  var promise = new Promise(function(resolve, reject) {
    //asynchronous code goes here
  });
}

Chúng ta bắt đầu với việc khởi tạo một đối tượng Promise và truyền vào đó một hàm callback. Hàm callback này sẽ nhận 2 tham số là resolvereject, với cả 2 tham số đều có kiểu là hàm (function). Tất cả các code bất đồng bộ sẽ nằm trong hàm callback này. Nếu mọi thứ thành công, Promise sẽ được hoàn thành và hàm resolve sẽ được gọi. Trong trường hợp có lỗi, hàm reject sẽ được gọi với một đối tượng Error, nhằm xác định rằng Promise này đã bị không thành công.

Giờ hãy thử xây dựng một ví dụ đơn giản để mô tả cách sử dụng Promise. Các dòng code sau sẽ tạo một request bất đồng bộ (asynchronous) đến một web service và trả về một đoạn JSON. Chúng ta sẽ tìm hiểu Promise hoạt động trong trường hợp này như thế nào

if (window.Promise) {
  console.log('Promise found');

  var promise = new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();

    request.open('GET', 'http://api.icndb.com/jokes/random');
    request.onload = function() {
      if (request.status == 200) {
        resolve(request.response); // we got data here, so resolve the Promise
      } else {
        reject(Error(request.statusText)); // status is not 200 OK, so reject
      }
    };

    request.onerror = function() {
      reject(Error('Error fetching data.')); // error occurred, reject the  Promise
    };

    request.send(); //send the request
  });

  console.log('Asynchronous request made.');

  promise.then(function(data) {
    console.log('Got data! Promise fulfilled.');
    document.getElementsByTagName('body')[0].textContent = JSON.parse(data).value.joke;
  }, function(error) {
    console.log('Promise rejected.');
    console.log(error.message);
  });
} else {
  console.log('Promise not available');
}

Ở đoạn code phía trên, phần khởi tạo Promise chứa các code bất đồng bộ dùng cho việc lấy dữ liệu từ web service thông qua API. Ở đây, chúng ta tạo ra một Ajax request đến đường link http://api.icndb.com/jokes/random, link này sẽ trả về một câu chuyện cười bất kỳ. Khi có response trả về (JSON string), nội dung của response sẽ được truyền và xử lý bởi hàm resolve(). Nếu có bất kỳ lỗi nào xảy ra, hàm reject() sẽ được gọi với input là đối tượng Error.

Khi chúng ta khởi tạo một đối tượng Promise, chúng ta có một proxy đến dữ liệu sẽ sẵn sàng sử dụng trong tương lai. Trong trường hợp cụ thể ở trên, chúng ta hy vọng rằng dữ liệu sẽ được truyền về từ phía web service vào một thời điểm nào đó. Vậy làm thế nào để chúng ta biết rằng khi nào thì dữ liệu sẽ sẵn sàng cho việc sử dụng? Đây là việc mà Promise.then() sẽ giải quyết cho chúng ta. Hàm then sẽ dùng 2 tham số với ý nghĩa: một callback thành công và một callback thất bại. Các callback này sẽ được gọi khi Promise được xử lý xong xuôi (thành công hay thất bại). Nếu Promise được xử lý trơn tru, callback thành công sẽ được gọi với dữ liệu truyền vào hàm resolve(). Nếu promise gặp lỗi, callback thất bại được goi. Bất kể thứ gì bạn truyền vào reject() sẽ được truyền như mội tham số đến callback này.

Code ở trên được demo ở đây. Bạn có thể refresh lại page để mỗi lần nhìn thấy một câu chuyện cười khác nhau. Đồng thời hãy thử bật console của trình duyệt, từ đó bạn sẽ thấy thứ tự thực hiện code. Lưu ý rằng một promise sẽ có 3 trạng thái :

  • Pending (đang xử lý)
  • Fulfilled (đã hoàn thành)
  • Rejected (đã bị từ chối)

Mỗi đối tượng Promise sẽ có một thuộc tính private chứa trạng thái hiện tại của Promise. Khi một Promise được hoàn thành hay từ chối, giá trị của thuộc tính này sẽ ngay lập tức được cập nhật với trạng thái của Promise. Điều này có nghĩa là một Promise chỉ có thể thành công hoặc thất bại một lần duy nhất. Nếu một Promise đã được hoành thành, và sau đó bạn mới gọi hàm then() của promise và truyền vào 2 callback, thì callback thành công sẽ luôn được gọi. Vì thế, trong thế giới của Promise, chúng ta không quan đến việc khi nào Promise được xử lý. Chúng ta chỉ quan tâm đến kết quả đầu ra của Promise.

Nối nhiều Promise

Sẽ có nhiều chúng ta muốn nối các promises với nhau. Ví dụ, bạn có thể có nhiều thao tác bất đồng bộ cần xử lý. Khi một thao tác trả về dữ liệu, bạn sẽ bắt đầu một xử lý bất đồng bộ khác sử dụng một phần dữ liệu từ thao tác trước đó, và cứ tiếp tục như vậy. Promise hỗ trợ việc nối các Promise với nhau giống như ví dụ dưới đây

function getPromise(url) {
  // trả về một Promise ở ddây
  // gửi một request lấy dữ liệu từ một url (request bất đồng bộ)
  // sau khi lấy về kết quả, xử lý promise với dữ liệu nhận được
}

var promise = getPromise('some url here');

promise.then(function(result) {
  //chúng ta có dữ liệu của url 'some url here' ở đây
  return getPromise(result); //và trả về một promise khác
}).then(function(result) {
  //ở đây chứa kết quả promise vừa trả về ở trên và logic để xử lý dữ liệu cuối cùng
});

Có một điều đáng lưu ý ở đây là khi một giá trị thông thường được trả về trong hàm then() đầu tiên, hàm then() thứ hai sẽ thực hiện với giá trị đó. Nhưng nếu bạn trả về 1 Promise ở hàm then() thứ nhất, thì hàm then() thứ hai sẽ chờ và chỉ được gọi cho đến khi Promise ở hàm then() thứ nhất thực hiện xong xuôi.

Xử lý lỗi

Trước đó tôi đã giới thiệu hàm then() sẽ nhận 2 hàm callbacks làm tham số. Hàm callback thứ 2 sẽ được gọi khi Promise bị từ chối. Tuy nhiên chúng ta cũng có một cách khác, sử dụng một hàm tên là catch(), chúng ta có thể xử lý khi Promise bị từ chối. Đoạn code dưới đây mô tả việc sử dụng hàm catch():

promise.then(function(result) {
  console.log('Got data!', result);
}).catch(function(error) {
  console.log('Error occurred!', error);
});

Đoạn ở trên tương đương với :

promise.then(function(result) {
  console.log('Got data!', result);
}).then(undefined, function(error) {
  console.log('Error occurred!', error);
});

Lưu ý rằng nếu Promise bị từ chối và hàm then() không có callback để xử lý việc này, xử lý sẽ được chuyển đến hàm xử lý lỗi của hàm then() tiếp theo hoặc hàm catch() tiếp theo. Ngoài việc dùng để xử lý Promise bị lỗi, hàm callback trong catch() cũng được gọi khi có bất kỳ exception nào được bắn ra từ callback của hàm khởi tạo Promise. Vì thế bạn có thể dùng catch() cho việc lưu log. Chú ý rằng chúng ta có thể sử dụng try...catch để xử lý lỗi, nhưng điều đó là không cần thiết khi sử dụng Promise vì bất kỳ exception nào cũng luôn được xử lý bởi hàm catch().

Tổng kết

Trên đây chỉ là những giới thiệu cơ bản về API Promise của Javascript. Khi nắm vững nó, bạn có thể viết code xử lý các thao tác bất đồng bộ một cách dễ dàng. Chúng ta có thể xử lý như bình thường mà không cần quan tâm giá trị sẽ được trả về sau đó trong tương lai. Có một vài API về Promise chưa được liệt kê ở đây. Để tìm hiểu thêm, các bạn có thể tham khảo một số tài liệu sau.