Một trong những cách tốt nhất để nâng cao trình độ code là đọc source code, đặc biệt là library/framework core code. Nếu so sánh việc đọc source code dự án của các Senior Developer như được cao thủ võ lâm truyền dạy, thì đọc library/framework core code còn bá đạo hơn, giống như đọc bí kíp võ công vậy.
Trong bài viết này, chúng ta sẽ tự xây dựng một mini Promise Library để hiểu rõ hơn về cơ chế hoạt động của Promise và Deferred trong JavaScript.
Promise và Deferred là gì?
Để hiểu rõ về Promise và Deferred, hãy lấy một ví dụ thực tế:
Đầu năm, bạn mượn của một người bạn một khoản tiền. Bạn nhận tiền và người bạn nhận được lời hứa là cuối năm bạn sẽ trả. Sự việc này là một hàm async, hành động bạn không trả tiền ngay được gọi là Deferred, và kết quả trả về là một Promise. Lúc này Promise.status = 'pending'
.
Đến cuối năm sẽ có hai trường hợp:
- Bạn trả tiền → Deferred được resolve → lời hứa được thực hiện →
Promise.status = 'fulfilled'
- Bạn không trả tiền → lời hứa không được thực hiện →
Promise.status = 'rejected'
Từ đầu năm, khi đồng ý cho mượn tiền, người bạn đã nghĩ đến hai trường hợp trên và đã đăng ký hai hàm callback tương ứng:
Promise.then(tinhCamAnhEmDiLen).catch(damVaoMatThangMuonTien);
- Nếu
Promise.status = 'fulfilled'
→ chạy hàmtinhCamAnhEmDiLen
- Nếu
Promise.status = 'rejected'
→ chạy hàmdamVaoMatThangMuonTien
Deferred object là đối tượng có thể tạo ra Promise và thay đổi trạng thái của nó. Deferred Object được sử dụng trong các hàm async và đóng vai trò là Producer of Value.
Promise object là một lời hứa, ban đầu chưa mang giá trị nào, nhưng vào một thời điểm nào đó trong tương lai sẽ có giá trị. Promise đóng vai trò là Receiver of Value.
Xây dựng Promise Object
Đầu tiên, chúng ta sẽ xây dựng Promise Object. 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 Deferred Object tùy theo trạng thái success/fail:
// Khởi tạo 2 mảng để lưu giữ callbacks
var Promise = function () {
this.successCallbacks = [];
this.errorCallbacks = [];
};
// Khi gọi hàm then(arg1, arg2) đẩy các callbacks vào mảng
Promise.prototype = {
successCallbacks: null,
errorCallbacks: null,
then: function (successCallback, errorCallback) {
this.successCallbacks.push(successCallback);
if (errorCallback) {
this.errorCallbacks.push(errorCallback);
}
return this; // Để hỗ trợ chaining
},
catch: function (errorCallback) {
this.errorCallbacks.push(errorCallback);
return this; // Để hỗ trợ chaining
},
};
Xây dựng Deferred Object
Deferred Object giữ nhiệm vụ thực thi callback đã được đăng ký trên Promise tùy vào trường hợp success hay error. Chúng ta sẽ sử dụng hai phương thức resolve()
và reject()
để xử lý các trường hợp này:
var Deferred = function () {
this.promise = new Promise();
};
Deferred.prototype = {
promise: null,
resolve: function (data) {
var self = this;
// Sử dụng setTimeout để đảm bảo tính async
window.setTimeout(function () {
self.promise.successCallbacks.forEach(function (callback) {
callback(data);
});
}, 0);
},
reject: function (error) {
var self = this;
// Sử dụng setTimeout để đảm bảo tính async
window.setTimeout(function () {
self.promise.errorCallbacks.forEach(function (callback) {
callback(error);
});
}, 0);
},
};
Chúng ta sử dụng 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 resolve/reject một Deferred. Đây là một phần quan trọng của Event Loop trong JavaScript.
Kiểm thử Promise và Deferred
Giờ chúng ta sẽ kiểm thử mini Promise Library của mình:
function test() {
var deferred = new Deferred();
// Giả lập một hàm bất đồng bộ, 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;
}
// Chạy test mỗi giây một lần
setInterval(function () {
test().then(
function (text) {
console.log(text);
},
function (error) {
console.log(error.message);
}
);
}, 1000);
Nâng cấp: Thêm Callback Chaining
Library của chúng ta vẫn còn đơn giản và chưa giải quyết được hai vấn đề quan trọng:
- Khi một Promise đã được thực hiện bằng Deferred, gọi hàm
then()
không có tác dụng - Không hỗ trợ callback chain
Để giải quyết vấn đề thứ nhất, chúng ta cần thêm trạng thái cho Promise:
var Promise = function () {
this.successCallbacks = [];
this.errorCallbacks = [];
this.status = "pending"; // Thêm trạng thái
this.value = null; // Giá trị khi resolve
this.reason = null; // Lý do khi reject
};
Và cập nhật phương thức then()
để xử lý trường hợp Promise đã được resolve hoặc reject:
then: function(successCallback, errorCallback) {
if (this.status === 'fulfilled' && successCallback) {
successCallback(this.value);
} else if (this.status === 'rejected' && errorCallback) {
errorCallback(this.reason);
} else {
if (successCallback) {
this.successCallbacks.push(successCallback);
}
if (errorCallback) {
this.errorCallbacks.push(errorCallback);
}
}
return this;
}
Để hỗ trợ callback chain, chúng ta cần thay đổi logic flow. Với mỗi hàm then()
, chúng ta cần tạo ra một Deferred mới:
then: function(successCallback, errorCallback) {
var deferred = new Deferred();
var successHandler = function(value) {
try {
var result = successCallback ? successCallback(value) : value;
if (result && typeof result.then === 'function') {
// Nếu result là một Promise, gắn deferred với Promise đó
result.then(function(value) {
deferred.resolve(value);
}, function(reason) {
deferred.reject(reason);
});
} else {
// Nếu không, resolve deferred với result
deferred.resolve(result);
}
} catch (e) {
deferred.reject(e);
}
};
var errorHandler = function(reason) {
try {
var result = errorCallback ? errorCallback(reason) : reason;
if (result && typeof result.then === 'function') {
// Nếu result là một Promise, gắn deferred với Promise đó
result.then(function(value) {
deferred.resolve(value);
}, function(reason) {
deferred.reject(reason);
});
} else {
// Nếu không, resolve deferred với result
deferred.resolve(result);
}
} catch (e) {
deferred.reject(e);
}
};
if (this.status === 'fulfilled') {
setTimeout(function() {
successHandler(this.value);
}.bind(this), 0);
} else if (this.status === 'rejected') {
setTimeout(function() {
errorHandler(this.reason);
}.bind(this), 0);
} else {
this.successCallbacks.push(successHandler);
this.errorCallbacks.push(errorHandler);
}
return deferred.promise;
}
Và cập nhật Deferred để thay đổi trạng thái của Promise:
resolve: function(data) {
if (this.promise.status !== 'pending') return;
this.promise.status = 'fulfilled';
this.promise.value = data;
var callbacks = this.promise.successCallbacks;
setTimeout(function() {
callbacks.forEach(function(callback) {
callback(data);
});
}, 0);
},
reject: function(reason) {
if (this.promise.status !== 'pending') return;
this.promise.status = 'rejected';
this.promise.reason = reason;
var callbacks = this.promise.errorCallbacks;
setTimeout(function() {
callbacks.forEach(function(callback) {
callback(reason);
});
}, 0);
}
Ví dụ với Callback Chaining
Giờ chúng ta có thể sử dụng callback chaining:
function asyncTask1() {
var deferred = new Deferred();
setTimeout(function () {
deferred.resolve("Task 1 completed");
}, 1000);
return deferred.promise;
}
function asyncTask2(result) {
var deferred = new Deferred();
setTimeout(function () {
deferred.resolve(result + " -> Task 2 completed");
}, 1000);
return deferred.promise;
}
asyncTask1()
.then(function (result) {
console.log(result); // 'Task 1 completed'
return asyncTask2(result);
})
.then(function (result) {
console.log(result); // 'Task 1 completed -> Task 2 completed'
});
Kết luận
Qua bài viết này, chúng ta đã tự xây dựng một mini Promise Library từ đầu, hiểu rõ hơn về cơ chế hoạt động của Promise và Deferred trong JavaScript. Mặc dù library của chúng ta còn đơn giản so với Promise trong ES6, nhưng nó đã giúp chúng ta hiểu được các khái niệm cơ bản và cách Promise hoạt động.
Trong thực tế, bạn nên sử dụng Promise có sẵn trong ES6 hoặc các thư viện như Q, Bluebird vì chúng đã được tối ưu hóa và xử lý nhiều edge case hơn. Tuy nhiên, việc tự xây dựng một mini Promise Library là một bài tập tuyệt vời để hiểu sâu hơn về JavaScript và các pattern async.
Hy vọng bài viết này giúp bạn hiểu rõ hơn về Promise và Deferred trong JavaScript!