[JavaScript] Tự xây dựng Promise/Defer với 100 dòng code

About

Sau vài tháng vọc vạch về mảng Infrastructure hôm nay bỗng thấy nhớ JavaScript, nên là quyết định tháng này làm 1 bài về JS cho đỡ nhớ 😃)

Tản mạn 1 chút, thường thì mọi người có nhiều cách để nâng cao trình độ code, có người thì làm side-project, người thì tham vấn các tutor, các bậc tiền bối đi trước. Bản thân mình thì thấy nâng cao level coding bằng việc đọc source code là 1 trong những cách hay nhất, và level cao hơn của đọc source code là đọc library/framework core code. Nếu so sánh việc đọc source code dự án của các Senior Developer viết ra giống như được cao thủ võ lâm truyền cho thì library/framework core code nó còn bá đạo hơn như là đọc bí kíp võ công vậy =))

Thường thì library/framework core code được rất nhiều các Developer tầm cỡ đóng góp, nên đây thực sự là những dòng code cực kỳ chất lượng, và đi kèm là sức sáng tạo tuyệt vời. Thường thì việc nghiền 1 module của 1 library/framework core code cũng ngốn kha khá thời gian đối với mình thậm chí nhiều khi đọc xong thấy nhũn não luôn nhưng đổi lại đọc xong thấy cái đầu mình được khai sáng thực sự.

Như đã biết trong vài năm qua Promise là 1 trong những Object nổi tiếng nhất trong JS, có thể nói là đi cùng với sự phát triển của JS luôn, mặc dù trong ES2016 đã định nghĩa thêm async/await function nhưng Promise đơn giản vẫn là base của mọi triển khai ECMA sau này.

Async functions always return Promises ---- http://exploringjs.com/es2016-es2017/ch_async-functions.html#_overview-2

Và thậm chí khi bạn có thể sử dụng thành thạo async/await functions, thì Promise vẫn giúp cho bạn giải quyết được vấn đề trong những hoàn cảnh đặc biệt Tham khảo.

Trong thời gian nghiền ngẫm về Promise trong ES6, jQuery, Q... thì mình cũng thử tự implement 1 cái Mini Promise Library xem sao.

Let's code

Promise và Deffered

Những ngày cuối năm, mọi người đều có vẻ sốt sắng thu hồi các khoản nợ nần, tự dưng thấy có gì đó liên quan đến khái niệm về PromiseDeffered => lấy luôn 1 ví dụ về chuyện này để giải thích các khái niệm này cho trực quan

  • Đầu năm, thì bạn có mượn của thằng bạn 1 món tiền, bạn nhận món tiền còn bạn của bạn nhận được lời hứa là cuối năm tao trả => sự việc này là 1 hàm async, hành động bạn không trả tiền ngay => được gọi là Deffered, kết quả trả về của nó là 1 Promise. Lúc này Promise.status = 'pending'
  • Đến cuối năm sẽ có 2 trường hợp:
    • Bạn trả tiền => Deffered được resolve => lời hứa được thực hiện => Promise.status = 'fulfilled'
    • Bạn không trả tiền (đừng như thế nhé) => lời hứa không được thực hiện => Promise.status = 'rejected'
  • Ok, nhưng mọi chuyện không đơn giản như vậy. Từ đầu năm khi đồng ý cho mượn tiền, khi nhận về Promise của bạn thì thằng bạn trong đầu đã nghĩ đến 2 trường hợp phía trên rồi, nên nó đã đăng ký ngay 2 hàm callback tùy theo việc bạn có thực hiện Promise không để thực thi như thế này Promise.then(tinhCamAnhEmDiLen).catch(damVaoMatThangMuonTien)
    • Nếu Promise.status = 'fulfilled' => chạy hàm tinhCamAnhEmDiLen
    • Nếu Promise.status = 'fulfilled' => chạy hàm damVaoMatThangMuonTien

Deferred object là đối tượng có thể tạo ra Promise và thay đổi trạng thái của status của nó, Deffered Object được sử dụng trong các hàm ASYNC và đóng vai trò là Producer of Value Promise Object là 1 lời hứa ,bản thân Promise khi được khởi tạo nó chưa mang giá trị nào, nhưng vào 1 thời điểm nào đó trong tương lai nó sẽ có giá trị - cũng giống như khi bạn hứa với chủ nợ là cuối năm tao sẽ trả nợ, còn việc có thực hiện (resolve) hay không (reject) là chuyện của tương lai =)) Hãy nhớ là tự bản thân Promise không thể thay đổi được trạng thái, mà phải do hành động phát sinh ra Promise thay đổi. Promise đóng vai trò là **Receiver of Value **

Minh họa bằng Diagram là như sau:

  1. Đăng ký hàm bất đồng bộ (ASYNC), trong hàm ASYNC cần tạo ra Deffered Object, đây là Object làm nhiệm vụ thông báo kết quả của hàm đã hoàn thành hoặc phát sinh lỗi. Hàm này sẽ trả kết quả về là 1 Promise Object.
  2. Trong Application code gọi tới hàm ASYNC thì đăng ký các callback với hàm.
  3. Hàm bất đồng bộ thực thi => tùy vào kết quả của Deffered Object mà gọi tới callback tương ứng trên Promise Object.
  4. Lặp lại tác vụ nếu đăng ký callback chain. Callback chain là như thế này:
new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
}).then(function(result) {
  return result * 2;
}).then(function(result) {
  return result * 2;
}).then(function(result) {
  return result * 2;
});

Native Promise Implementation w/o Callback Chaining

Để cho đơn giản chúng ta xây dựng mô hình chưa có khả năng chaining callback trước.

Promise Object

Đầu tiên là Promise Object. Bởi vì Promise cần có khả năng lưu giữ các callback truyền vào trong hàm then() những hàm callback này sẽ được thực thi bởi Deffered Object tùy theo trạng thái success/fail, theo suy nghĩ đó chúng ta implement như sau:

//khởi tạo 2 mảng để lưu giữ callbacks truyền vào
var Promise = function () {
  this.successCallbacks = [];
  this.errorCallbacks = [];
};

//khi gọi hàm then(arg1, arg2) đẩy các callbacks được đăng ký vào mảng khởi tạo
Promise.prototype = {
  successCallbacks: null,
  errorCallbacks: null,
  then: function (successCallback, errorCallback) {
    this.successCallbacks.push(successCallback);
    if (errorCallback) {
      this.errorCallbacks.push(errorCallback);
    }
  }
};

Deffered Object

Deffered Object giữ nhiệm vụ thực thi callback đã được đăng ký trên Promise tùy vào khi trường hợp success hay error. Để cho giống với các thư viện tiêu chuẩn, mình dùng 2 thuật ngữ resolve()reject() để đặt tên các function này.

// Ngoài 2 hàm resolve() và reject() thí mỗi thể hiện của Deffered sẽ có 1 Promise đính kèm với nó, ta tạo 1 property để lưu Promise lại
Deferred.prototype = {
  promise: null,
  resolve: function (data) {
    this.promise.okCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(data)
      }, 0);
    });
  },
  reject: function (error) {
    this.promise.koCallbacks.forEach(function(callback) {
      window.setTimeout(function () {
        callback(error)
      }, 0);
    });
  }
};
// Dùng hàm setTimeout để đẩy các callback vào hàng đợi.
// Đảm bảo code ứng dụng vẫn được thực hiện sau khi bạn resolve/reject 1 Deffered
// (nếu bạn chưa rõ đoạn này hãy tìm hiểu thêm về EventLoop nhé)

Run it

Về cơ bản cái Promise của chúng ta đã có thể sử dụng được rồi, test thử nào. Đầu tiên như hình flow ở đầu bài, cần phải đăng ký 1 ASYNC function có khả năng khởi tạo Deffered Object cũng như trả về Promise đã.

function test() {
  var deferred = new Deferred();
  // Giả lập 1 hàm bất đồng bộ, mỗi lần sẽ trả về resolve hoặc reject ngẫu nhiên
  setTimeout(function () {
    var random_boolean = Math.random() >= 0.5;
    if (random_boolean) {
      deferred.resolve('success');
    } else {
      deferred.reject(new Error('error'));
    }
  }, 100);
  return deferred.promise;
}

// 1 giây chạy 1 lần
setInterval(function() {
   test().then(function (text) {
      console.log(text);
    }, function (error) {
      console.log(error.message);
    }); 
}, 1000);

Kêt quả:

Adding callbacks chaining

Library của mình lúc này vẫn còn quá đơn giản, nó chưa giải quyết được 2 vấn đề quan trọng: * Khi 1 Promise đã được thực hiện bằng Defer, gọi hàm then() sẽ không có tác dụng gì. * Không hỗ trợ callback chain Bằng việc thêm status cho Promise thì khi 1 Promise đã được thực hiện, gọi hàm then() giúp chúng ta thực thi được ngay callback mà không cần thông qua Defer. Để implement callback chain, chúng ta cần thay đổi 1 chút về logic flow, với mỗi hàm then(), cần tạo ra 1 Defer mới. Khi Promise được resolved/rejected, dựa vào kết quả trả về của callback mà ta ta cũng resolve Defer vừa được tạo ra hoặc gắn Defer đó với Promise từ callback trả về với

Đây là phần khó nhất và cũng khá phức tạp , mình sẽ cố gắng vẽ flow và comment từng đoạn code để các bạn có thể hiểu được.

Program Flow

Promise Object

var Promise = function () {
  this.successCallbacks  = [];
  this.errorCallbacks  = [];
};

Promise.prototype = {
  successCallbacks: null,
  errorCallbacks: null,
  status: 'pending',
  error: null,

  then: function (successCallback, errorCallback) {
    //Defer này sẽ được resolve sau khi callback hiện tại được thực thi
    var defer = new Defer();

    // Lưu giữ Defer tiếp theo sẽ được resolve kèm với callback
    this.successCallbacks.push({
      func: successCallback,
      defer: defer
    });

    if (errorCallback) {
      this.errorCallbacks.push({
        func: errorCallback,
        defer: defer
      });
    }

    // Nếu Promise đã có kết quả thì thực hiện ngay callback không cần quan tâm đến Defer
    if (this.status === 'resolved') {
      this.executeCallback({
        func: successCallback,
        defer: defer
      }, this.data)
    } else if(this.status === 'rejected') {
      this.executeCallback({
        func: errorCallback,
        defer: defer
      }, this.error)
    }
    
    // Vẫn như trước, trả về promise hiện tại
    return defer.promise;
  },

  // Đẩy callback vào queue để thực hiện dần
  executeCallback: function (callbackData, result) {
    window.setTimeout(function () {
      var res = callbackData.func(result);
      if (res instanceof Promise) {
        // Nếu kết quả của callback là 1 Promise, gắn defer mới Promise này
        // Khi Promise được resolve thì defer cũng sẽ được resolve
        // Kéo theo callback chain được thực thi
        callbackData.defer.bind(res);
      } else {
        // Nếu không thì resolve luôn Defer mới
        // Kết thúc chuỗi
        callbackData.defer.resolve(res);
      }
    }, 0);
  }
};

Defer Object

var Defer = function () {
  this.promise = new Promise();
};

Defer.prototype = {
  promise: null,
  resolve: function (data) {
    // Update trạng thái của Promise hiện tại
    // Thực thi callback
    // Truyền vào data kết quả
    var promise = this.promise;
    promise.data = data;
    promise.status = 'resolved';
    promise.successCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, data);
    });
  },

  reject: function (error) {
    // Update trạng thái của Promise hiện tại
    // Thực thi callback
    // Truyền vào data kết quả
    var promise = this.promise;
    promise.error = error;
    promise.status = 'rejected';
    promise.errorCallbacks.forEach(function(callbackData) {
      promise.executeCallback(callbackData, error);
    });
  },

  // Gắn Defer hiện tại với kết quả của Promise trả về từ callback
  // Đăng ký callback với Promise đó
  bind: function (promise) {
    var that = this;
    promise.then(function (res) {
      that.resolve(res);
    }, function (err) {
      that.reject(err);
    })
  }
};

Run it

Thay vì 1 hàm ASYNC, lần này chúng ta sẽ nối 2 hàm ASYNC với nhau, đây là công việc hàng ngày các bạn sẽ phải thực hiện. Yêu cầu đặt ra là các hàm phía sau phải chờ hàm trước nó được thực hiện xong.

function before() {
  var defer = new Defer();
  // an example of an async call
  setTimeout(function () {
    var random_boolean = Math.random() >= 0.5;
    if (random_boolean) {
      defer.resolve('success before');
    } else {
      defer.reject(new Error('error'));
    }
  }, 100);
  return defer.promise;
}

function after() {
  var defer = new Defer();
  // an example of an async call
  setTimeout(function () {
    var random_boolean = Math.random() >= 0.5;
    if (random_boolean) {
      defer.resolve('success after');
    } else {
      defer.reject(new Error('error'));
    }
  }, 100);
  return defer.promise;
}

before().then(function (text) {
  console.log(text);
  // Thay vì kết thúc luôn, trả về 1 Promise tiếp
  return after();
}, function (error) {
  console.log(error.message);
}).then(function (text) {
    console.log(text)
}); 

Và đây là kết quả

Hê hê, có vẻ mọi thứ run smoothly rồi đấy. Mặc dù chỉ 100 dòng code nhưng đừng coi thường nhé, đây là cách triển khai tuân thủ đúng CommonJS Promises/A Proposal Specification, có thể kể ra rất nhiều thư viện nổi tiếng đang sử dụng: Q, when.js, jQuery 1.5 , mocha, chai,... Tham khảo. Nếu chưa thực sự hiểu rõ, cố gắng đọc code và chạy thử vài lần nhé 😃 Mình cũng đính kèm 1 repo tại Github Repo

...

Qua bài này, mình đã hướng dẫn cho các bạn xây dựng 1 cái mini Promise Library với 100 dòng code - chính xác thì khoảng 110 dòng - mặc dù nó vẫn chưa đầy đủ các tính năng như các thư viện thật như khả năng bắt lỗi v..v... nhưng cũng đã có thể đáp ứng được trong những bài toán nhất định. Như đã nói hiểu được cách hoạt động của core-code là 1 phương pháp học tập rất tốt, khi hiểu được bản chất vấn đề việc học API/syntax là rất nhanh ^^ Mình rất khuyến cáo các bạn đọc core sau đó implement lại theo ý hiểu của mình. Cảm ơn anh em đã đọc và ủng hộ (up)