Javascript promise
Javascript
274
es6
21
promise
6
White

Hà Phạm viết ngày 15/07/2015

Đây là một trong các concept mới đối tượng mới được đưa vào ECMAScript 6. Việc sử dụng chúng rất dễ nhưng để hiểu được thì (đối với tôi) cũng cần kha khá thời gian nên tôi phải lưu lại đây.

Tác vụ không đồng bộ

Thành thật mà nói tôi không thể cung cấp một định nghĩa cụ thể và chính xác. Tôi chỉ có thể nhận ra một tác vụ là không đồng bộ dựa vào dấu hiệu, đó là một tác vụ sẽ hoàn thành trong tương lai mà tôi không cần chờ đợi trong khi nó được thực hiện. Ví dụ:

setTimeout(function whenItCome() {
    console.log('time\'s up ');
}, 1000);
console.log('I go first');

// I go first
// time's up

Bạn có thể đọc thêm về cách JS xử lý bất đồng bộ ở đây, nhưng khoan, hãy đọc nó sau (ấy là trong trường hợp bạn không biết).

Vậy mỗi tác vụ không đồng bộ sẽ có một hàm xử lý - "task handler" được thực thi nhiều nhất là một lần khi mà tác vụ đã hoàn tất. Ví dụ đoạn mã ở trên, setTimeout là tác vụ, còn hàm whenItCome là task handler, hay thường gọi là callback. Nhưng hãy cẩn thận, callback không phải lúc nào cũng là task handler của một tác vụ không đồng bộ. Khi bạn viết setTimeout(callback, 10000) thì bạn muốn callback được gọi sau ít nhất là 10s kể từ khi hàm setTimeout trả về (return). Nhưng khi viết [1, 2, 3].forEach(doSomeStuff) thì điều tôi mong đợi khi hàm forEach trả về là doSomeStuff đã được thực thi với tất cả các phần tử trong mảng. Do vậy doSomeStuff là một callback đồng bộ.

Quay về với tác vụ không đồng bộ, nếu bây giờ trong hàm xử lý, tôi lại thực hiện một tác vụ không đồng bộ nữa

setTimeout(function() {
   ajaxCall('/api/abc', function(data){
       // và có khi lại gọi một tác vụ không đồng bộ nữa.
   });
}, 2000);

Dễ thấy đoạn mã nhanh chóng trở nên xấu xí và khó đọc, cũng như tiềm ẩn lỗi, mà chúng ta sẽ gọi là pyramid of doom hoặc the callback hell. Hãy xem đoạn code ma quái tiêu biểu sau đây:

function requestHandler(params, callback) {
    var cachedData = 'some data';
    if(Math.random() < 0.5) {
        callback(cachedData);
    } else {
        askDatabase(params, function(data){
            callback(data);
        });
    }
}

Ở đây có 2 trường hợp gọi callback, nhưng ở trường hợp 1 (dòng 4), chúng ta đã gọi callback đồng bộ, tức callback này trả về trước khi requestHandler return, còn ở trường hợp 2 là callback không đồng bộ, callback sẽ trả về sau khi requestHandler return. Đây chính xác là cách Zalgo xuất hiện như Isaac Z. Schlueter đã mô tả. Vậy promise được tạo ra để giải quyết vấn đề này.

Promise là gì?

Hãy chú ý đến các tác vụ không đồng bộ, chúng sẽ trả về giá trị là gì? Void, null, 1, 'abc', gì mà chả được, điều đó chẳng quan trọng, bởi những giá trị nó trả về sẽ được truyền luôn cho callback của nó.
Nhưng điều đó không còn đúng khi promise ra đời.

Promise là một đối tượng, là kết quả của một tác vụ không đồng bộ. Như vậy trong một thế giới lý tưởng, không còn chiến tranh và nghèo đói, tất cả các tác vụ không đồng bộ khi được gọi sẽ trả về một promise.

Khởi tạo promise cách chính thống sẽ như thế này

var promise = new Promise (function a(resolve, reject) {
    if(// task complete) {
        resolve(value);
    } else {
        reject(new Error());
    }
});

Phương thức khởi tạo chỉ có 1 tham số là một hàm thực thi (executor). Về phía hàm thực thi lại nhận 2 hàm callback làm tham số:

  • resolve: khi tác vụ không đồng bộ thành công thì hàm resolve được gọi, tham số của nó là kết quả tính toán của tác vụ không đồng bộ.
  • reject: được gọi khi tác vụ thất bại, tham số nó nhận có thể là một đối tượng lỗi.

Cả 2 hàm callback này đều là callback không đồng bộ. Điều đó có nghĩa là chúng được thực thi sau khi executor đã thực thi xong.

Promise theo chuẩn A+ sẽ có đặc điểm như sau:

1. Trạng thái

Tại 1 thời điểm, 1 promise sẽ có 1 trong 3 trạng thái:

  • pending: kết quả chưa được xử lý xong, đang chờ.
  • fulfilled: tác vụ thực hiện thành công
  • rejected: tác vụ không đồng bộ đã thất bại.

2 trạng thái cuối được gọi chung là settled. Tất nhiên promise chỉ có thể chuyển từ pending sang settled, không có chiều ngược lại.

Promise khi khởi tạo sẽ có ngay trạng thái pending, sau khi chuyển sang settled thì giữ nguyên trạng thái đó (fulfilled hoặc rejected). Promise có thể bị rejected khi ta gọi hàm reject hoặc khi có một ngoại lệ (exception) được tung ra.

Chú ý là một promise chỉ được settled 1 lần duy nhất.

2. Promise là thenable

Thenable là cái gì, thenable đơn giản là một đối tượng có phương thức then. Phương thức này sẽ nhận 1 hoặc 2 tham số, đều là các hàm callback.

aPromise.then(function onFulfill(value) {
    // do something with value
}[, function onReject(reason) {
    // handle error
}]);

/* dấu ngoặc vuông chỉ ra rằng đoạn code đó là tùy chọn, không phải mảng, thanks @tovin07 */

Đúng như tên gọi, onFulfill được gọi khi promise fulfill, hay khi tác vụ không đồng bộ thành công, ngược lại nếu tác vụ thất bại thì onReject được gọi. Callback onReject là tùy chọn, bạn có thể xử lý lỗi, hoặc không làm gì cũng là một cách đối mặt.

Nhưng để thenable có thể là promise thì thenable đó phải thỏa mãn:

  • Hàm then lại trả về một promise khác.
  • Nếu hàm then trả về một giá trị không thenable thì giá trị đó được chuyển thành một promise được fulfill ngay lập tức.

2 điều trên cho phép một promise được then liên hoàn (chaining) một cách tuần tự.

aPromise
    .then(function(){
        // do abc
    })
    .then(function() {
        // do more
    });
  • Hàm then có thể được gọi nhiều lần. Khi promise được settle thì toàn bộ các callback tương ứng sẽ được gọi.
aPromise.then(doThing);
aPromise.then(doOtherThing);
aPromise.then(doFkingOtherMore, catchAFkingError);

3. Bắt lỗi với catch

catch cũng là một phương thức của promise giống như then nhưng nó chỉ được dùng để bắt lỗi.

aPromise.catch(doThisWhenItRain);

// sẽ giống với
aPromise.then(null, doThisWhenItRain);

Hầu như đây chỉ là cách để viết cho đẹp, hoàn toàn không có khác biệt nào giữa 2 cách gọi. Tức là bạn vẫn có thể bắt lỗi liên hoàn hoặc song song giống với then.

Tạm kết

Uầy, đã hơn nghìn chữ rồi, lằng nhằng phết. Tôi sẽ cập nhật một số ví dụ để làm rõ hơn về promise sau 1 . Tóm lại những thứ cần nhớ ở bày này là:

  • Tác vụ không đồng bộ được thực thi mà bạn không cần chờ đợi, khi nó thực hiện xong sẽ gọi hàm callback để xử lý kết quả.
  • Callback có thể là đồng bộ hoặc không đồng bộ.
  • Promise là kết quả mà lời gọi tác vụ không đồng bộ trả về, đại diện cho kết quả của tác vụ không đồng bộ.
  • Tiêu thụ kết quả (trong tương lai) của tác vụ không đồng bộ thông qua việc gọi hàm then của promise.

UPDATE

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Hà Phạm

25 bài viết.
66 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
43 8
Xin chào, đây là lần đầu tiên mình post bài ở đây. Nhiều vấn đề mình cũng không rành lắm, có gì sai mọi người góp ý nhé. Xin cảm ơn :D Bài này gi...
Hà Phạm viết hơn 3 năm trước
43 8
White
34 8
Lâu không post gì muốn viết một bài dài dài về js cơ mà đau đầu quá viết mãi không xong, thôi post bài ngắn vậy :smiley: Lấy screen size ở đây tôi...
Hà Phạm viết 3 năm trước
34 8
White
16 1
Tiếp tục loạt bài về promise, như tôi đã hứa ở cuối (Link). Hả? Không, còn mỗi bài này nữa thôi, tôi thề. Bài trước chúng ta đã xem xét tác vụ khô...
Hà Phạm viết hơn 3 năm trước
16 1
Bài viết liên quan
White
5 1
Xử lý đồng bộ một mảng bằng Promise thay cho async.eachSeries Tựa Đang muốn chạy một hàm trong đó xử lý đồng bộ từng phần tử trong một mảng, do g...
Cuong Pham viết 2 năm trước
5 1
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
25 bài viết.
66 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!