Muốn hiểu async trong JavaScript tới nơi tới chốn thì có một cách khá hiệu quả: tự viết lại một phiên bản Promise đủ nhỏ để mình nhìn thấy hết state, callback queue và cách chaining vận hành.
Mình sẽ đi theo đúng cách đó: dựng một mini Promise Library để bóc tách Promise và Deferred vận hành bên dưới như thế nào.
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, mình 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. Mình 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);
},
};
Mình dùng setTimeout để đẩy callback sang một lượt chạy sau, đảm bảo chúng không thực thi ngay lập tức. Nó giúp mô phỏng tính bất đồng bộ cho bài tập này, nhưng lưu ý một điểm quan trọng: Promise native thật dùng microtask queue, không phải macrotask từ setTimeout.
Kiểm thử Promise và Deferred
Giờ mình sẽ kiểm thử mini Promise Library:
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 mình 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, mình 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, mình cần thay đổi logic flow. Với mỗi hàm then(), mình 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);
}
Chuỗi async task với callback chaining
Giờ mình có thể 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'
});
Xây từ đầu để hiểu, dùng thật thì dùng Promise native
Tự viết lại như vậy không phải để thay Promise native trong code thật. Giá trị chính của bài tập này là mình nhìn thấy rõ hơn vì sao Promise cần state, vì sao callback không chạy ngay, và vì sao chaining lại đòi hỏi thêm một Deferred ở giữa.
Trong production thì cứ dùng Promise native hoặc thư viện trưởng thành nếu bài toán cần thêm tiện ích. Nhưng nếu một ngày bạn thấy then, catch, resolve, reject vẫn còn hơi mơ hồ, tự dựng lại một bản nhỏ như cách vừa làm là bài tập khá đáng công.