jQuery: Deferred - Promise - căn bản

Bài này tôi viết có sự tham khảo tài liệu sau:

爆速でわかるjQuery.Deferred超入門

jQuery.Deferred là một object có từ phiên bản 1.5 dùng để xử lý vấn đề bất đồng bộ. Dưới đây là các ưu điểm của nó:

  1. Liên kết (chain) các xử lý bất đồng bộ, giúp giải quyết vấn đề Callback hell
  2. Dễ dàng xử lý lỗi
  3. Có thể gom các xử lý bất đồng bộ trong một function nên dễ tái sử dụng.

Deferred và Promise

Đôi dòng giải thích ngữ nghĩa

Deferred

Deferred khi dịch sang tiếng Việt có nghĩa là “trì hoãn”. Vì một Bất đồng bộ không phải xảy ra tức thì mà nó có thể “trì hoãn” và xảy ra bất kỳ thời điểm nào trong tương lai mà ta chưa biết trước được. “Trì hoãn” cũng giống như một công việc mà cần có thời gian để hoàn thành. Công việc đó sẽ mang các trạng thái “Đang thực hiện”, “Đạt”, “Không đạt”.

Promise

Promise khi dịch sang tiếng Việt có nghĩa là “khế ước”, chứa các điều khoản giữa bên A và bên B, chủ yếu ở các điều khoản: “Đạt”, “Không đạt”. Tôi trích nội dung trong một khế ước nghiệm thu như sau:

  1. Khi công trình XYZ kết thúc, nếu được đánh giá “Đạt” thì bên A thanh toán đầy đủ kinh phí cho bên B theo quy định tại Hợp đồng này.
  2. Khi công trình XYZ kết thúc, nếu được đánh giá “Không đạt” thì bên B có trách nhiệm hoàn trả toàn bộ kinh phí … và …

Theo trên, “khế ước” sẽ được xử lý theo trạng thái “Đạt” và “Không đạt” của công trình XYZ.

Deferred và Promise trong jQuery.

Trong jQuery, Deferred là một Object được tạo bởi function $.Deferred():

var d = new $.Deferred();

Khi Object Deferred được tạo thì object Promise cũng tự động sinh ra. Object Deferred bao bọc object Promise.

-----------------
|   Deferred    |
|               |
|               |
|  ------------ |
|  |  Promise | |
|  |----------| |
|_______________|

Và để xử lý các Bất động bộ, cần thực hiện trình tự theo các bước sau:

jquery-deferred

  1. Tạo object Deferred
  2. Start function bất đồng bộ và truyền function thay đổi trạng thái của Deferred (công việc XYZ) vào trong async operaton.
  3. Trả về Promise

Tất nhiên, những function bất đồng bộ của jQuery như $.ajax(), $.getJSON() thì từ đầu đã qua trình tự thiết lập trên rồi, nên ta không cần phải thực hiện lại nữa.

Cấu trúc object Promise

jqueryDeferred1

Về cơ bản, một Promise gồm có:

  • Trạng thái (.state)
  • Callback .done() được thực thi khi trạng thái là resolved
  • Callback .fail() được thực thi khi trạng thái là rejected

Bên dưới là cấu trúc thực tế của Object Promise:

promise.png

Trong đó có 2 Callback function .done(), .fail() và function .then() .

Thực nghiệm

Source code:

<html>
<head>
    <title></title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
</head>
<body>
    <script type="text/javascript">
        /**
         * Delay 1 giây rồi log ra Hello! rồi gọi function resolve của Deferred.
         *
         * @returns Promise
         */
        function delayHello()
        {
          var d = new $.Deferred;
          setTimeout(function(){
            console.log('Hello!');
            d.resolve();
          }, 1000);
          return d.promise();
        }
        /**
         * Delay 1 giây rồi log ra Hello! rồi gọi function reject của Deferred.
         *
         * @returns Promise
         */
        function delayError() {
          var d = new $.Deferred;
          setTimeout(function(){
            d.reject('Error!!');
          }, 1000);
          return d.promise();
        }
        function hello1() {
          console.log('Hello sync1');
        }

        function hello2() {
          console.log('Hello sync2');
        }
    </script>
</body>
</html>

.done() và .fail()

Bạn hãy tạo file jqueryDeferred.html chứa source code trện, rồi hãy mở Chrome > Developer Tool, trong tab Console, hãy thử:

var sucess = delayHello(); 
sucess.done(hello1);

//Output
> Object {}
> Hello!
> Hello sync1

sucess.fail(hello1);

//Output
> Object {}


var error = delayError(); 
error.fail(hello1);

//Output
> Object {}
> Hello sync1

function delayHello gọi xử lý resolve() của Deferred, cho nên Callback .done() được thực thi, còn Callback .fail() không được thực thi.

resolve

function delayError gọi xử lý reject() của Deferred, cho nên Callback .fail() được thực thi.

resolve

.then()

Khi sử dụng .then(), ta có thể đăng ký cả 2 callback .done().fail() đồng thời.

.then(
    function(){} /* done */
,   function(){} /* fail */
)

Do đó, nếu không truyền vào callback function thứ 2 thì mặc định nó chỉ đăng ký callback .done thôi

.then(
    function(){} /* done */
)

Đoạn thực nghiệm dưới đây sẽ minh họa ý trên.

delayHello().then(hello1, hello2);

// Output
> Hello!
> Hello sync1

delayHello().then(hello1);

// Output
> Hello!
> Hello sync1

delayError().then(hello1, hello2);
// Output
> Hello sync2

Nếu chỉ để xử lý cho duy nhất một Bất đồng bộ thì sẽ không thấy hết lợi ích của jQuery.Deferred, lợi ích của jQuery.Deferred thể hiện khi xử lý theo dạng chain. Phần tiếp theo sẽ giải thích rõ hơn chain trong Deferred.

.then() chain 1: TH chain các function thông thường

.then() không những dùng để đăng ký .done(), .fail() mà còn trả về một object Promise mới chứ không phải chỉ là this trỏ đến một object Promise.

promise-then.png

(done, fail cũng trả về một object Promise mới)

Ta cũng có thể xác nhận điều đó bằng so sánh giá trị trả về từ các lần gọi .then().

var p1 = delayHello();
var p2 = p1.then(hello1);
var p3 = p2.then(hello1);

console.log(p1 === p2); //false
console.log(p2 === p3); //false
console.log(p3 === p1); //false

Nếu ta không quan tâm đến object trả về giữa chừng:

delayHello()
.then(hello1)
.then(hello1);

// Output
> Hello!
> Hello sync1
> Hello sync1

// Hello sync1 đồng loạt cùng output

Đoạn code trên sẽ tạo ra 3 Promise có trạng thái liên kết với nhau.

1 Promise được sinh ra từ việc tạo Deferred, và 2 Promise còn lại được trả về từ .then()

Promise trả về của .then() sẽ tự động kế thừa trạng thái của Promise trước nó -> Trạng thái của Promise sẽ liên kết nhau. Khi trạng thái Promise trả về của delayHello() là “resolved” thì những Promise sau nó cũng sẽ trở thành “resolved” và các xử lý tương ứng cũng sẽ đồng loạt cùng xử lý.

delayError()
.then(hello1)
.then(hello1);

// Output
-- nothing --

Tương tự như vậy, khi trạng thái Promise trả về của delayError() là “reject” thì những Promise sau nó cũng sẽ trở thành “reject” và các xử lý tương ứng cũng sẽ đồng loạt cùng xử lý. Tuy nhiên trong đoạn code trên, các xử lý cho TH fail không được truyền vào cho nên nó bỏ qua hết.

.then() chain 2:TH liên kết các function có trả về Promise

Như đã nêu ở trên, ở jQuery.Deferred thì function bất đồng bộ, chúng ta cần có bước “Trả về Promise”.
.then() trả về Promise cho nên ta có thể liên kết các xử lý bất đồng bộ lại với nhau, do đó ta có thể thực thi việc xử lý bất đồng bộ theo dạng trình tự từng bước, không cần phải lồng xử lý trong xử lý.

delayHello()
.then(delayHello)
.then(delayHello)
.then(delayHello)
.then(delayHello);

//Output: cứ sau mỗi 1 giây, in ra 'Hello!'

Cấu trúc các object Promise sinh ra trong quá trình này sẽ phức tạp hơn so với TH 1
Dưới đây là hình minh họa khi liên kết 2 delayHello:

delayHello().then(delayHello);

Khác với TH1, trong TH này có tới 3 object Promise.
delayHello() đầu tiên trả về 1 Promise (p1), .done của Promise này sẽ trả về 1 Promise thứ 2 (p2), và Promise thứ 3 (p3) thì được sinh ra theo .then(). Trạng thái p2 sẽ ngay tức khắc truyền cho p3, nên ta cũng không cần quan tâm đến p2 cho lắm.

Trong TH ta sử dụng .done thay cho .then thì nó giống như việc ta đăng ký 2 callback done cho cùng một trạng thái, cho nên khi trạng thái chuyển sang done thì 2 callback này sẽ cũng thực thi.

delayHello().done(delayHello).done(delayHello);

//Output
Chờ 1 giây, output ra `Hello!`
Sau 1 giây nữa thì output cùng lúc 2 `Hello!` 

Cấu trúc Promise sẽ như sau:

Khi liên kết bằng .then():

delayHello().then(delayHello).then(delayHello);

//Out ra `Hello!` theo trình tự 1 giây

.then() chain 3:TH sửa lỗi khi có lỗi.

Với Promise

delayError()
.then(hello1,
  function(e){
    console.log(e);
    console.log('Fixed from Error');
    return new $.Deferred().resolve().promise();
  })
.then(hello1, hello2);

// Output
1 giây sau
'Error!!'
'Fixed from Error'
'Hello sync2'

Dùng $.when() để liên kết song song:

$.when() dùng để tập hợp các Promise và trả về 1 Promise mới. Vì $.when() trả về một object Promise, cho nên ra cũng có thể sử dụng .then(), .done(), .fail() để liên kết:

$.when(delayHello(), delayHello(), delayHello())
.done(hello1);

Hình minh họa:

Nếu tất cả các Promise đều là “Resolved” thì Promise trả về từ $.when() sẽ là “Resolved”. Ngược lại, chỉ cần 1 Promise là “Pending” thì xử lý cũng sẽ pending. Còn nếu chỉ 1 trong các Promise là “Rejected” thì ngay tức khắc Promise trả về từ $.when() sẽ là “Rejected”.

TH Resolved:

TH Rejected:

Trong TH rejected, cho dù Promise trả về từ $.when() là “Rejected” đi nữa nhưng các xử lý song song trong nó vẫn tiếp tục thực hiện. Nếu ta muốn stop việc xử lý này lại thì ta có thể dùng logic sửa lỗi, đăng ký xử lý cho TH .fail().

Function hóa

Trong chuỗi .then(), ta có thể chia các phần xử lý thành các function để có thể tái sử dụng.
Ví dụ:

delayHello()
.then(delayHello)
.then(function(){
  return $.when(delayHello(), delayHello(), delayHello());
})
.then(delayHello)
.then(function(){
  return $.when(delayHello(), delayHello(), delayHello());
})
.then(delayHello);


// Gôm phần xử lý như nhau thành 1 function:
function delayHelloParallel() {
  return $.when(
    delayHello(), delayHello(), delayHello()
  )
  .then(delayHello);
}

//Sử dụng delayHelloParallel trên:
delayHello()
.then(delayHello)
.then(delayHelloParallel)
.then(delayHelloParallel)