Promises trong JavaScript cung cấp một cách mạnh mẽ để quản lý luồng bất đồng bộ, nhưng một trong những hạn chế của chúng là không hỗ trợ trực tiếp cho các bản đồ hoặc hash của promises. Trong bài viết này, chúng ta sẽ khám phá cách sử dụng Promise.all() để xử lý một đối tượng chứa nhiều promises, mà tôi gọi là "promise hash". Điều này sẽ giúp mã của bạn trở nên rõ ràng hơn và dễ bảo trì hơn.
Yêu Cầu
Để xây dựng một hàm promiseHash, chúng ta cần thực hiện một số yêu cầu sau:
- Chấp nhận một "promise hash" - một đối tượng với các cặp key-value, trong đó giá trị có thể là promises.
- Trích xuất các giá trị thành một mảng và truyền chúng vào
Promise.all(). - Nếu thành công, kết nối lại mảng kết quả với các key ban đầu và trả về "value hash" mới. Nếu không, chuyển tiếp sự từ chối.
- Trả về chuỗi promise chứa đối tượng "value hash".
Không Phải Tất Cả Đều Là Promises
Lưu ý rằng từ có thể trong yêu cầu đầu tiên. Nếu bạn không quen thuộc với cách hoạt động bên trong của Promise.all(), bạn có thể chưa biết rằng nó chấp nhận cả các giá trị không phải là promises và xử lý chúng như các promises đã được giải quyết. Điều này mang lại sự linh hoạt, cho phép một hàm có thể là bất đồng bộ hoặc không.
javascript
Promise.all([
Promise.resolve(1),
2,
new Promise(res => setTimeout(res, 100, 3)),
(a) => a + 2,
]).then(console.log);
// [1, 2, 3, ƒ() ]
Điều này không phải là một vấn đề bạn cần lo lắng, nhưng nó có thể cực kỳ hữu ích trong việc kiểm tra và mô phỏng, vì bạn không cần phải tạo các hàm bất đồng bộ nếu chúng không khởi tạo các chuỗi promise.
Lập Trình
Bây giờ, hãy xây dựng hàm promiseHash dựa trên các yêu cầu của chúng ta.
Chấp nhận một "promise hash"
Đầu tiên, chúng ta cần khởi tạo hàm với tham số đầu vào.
javascript
const promiseHash = (inputHash) => {};
Trích xuất các giá trị để chuyển cho Promise.all()
Các phiên bản mới của JavaScript làm điều này rất đơn giản:
javascript
const promiseHash = (inputHash) => {
return Promise.all(Object.values(inputHash));
};
Nếu thành công, trả về "value hash" mới
javascript
const promiseHash = (inputHash) => {
return Promise.all(Object.values(inputHash))
.then(results => {
const keys = Object.keys(inputHash);
const entries = results
.map((value, index) => [keys[index], value]);
return Object.fromEntries(entries);
});
};
Chúng ta không cần xử lý việc từ chối ở đây vì Promise.all() tự động chuyển tiếp sự từ chối của promise đầu tiên mà gặp phải.
Trả về chuỗi promise
javascript
const promiseHash = (inputHash) => {
return Promise.all(Object.values(inputHash))
.then(results => {
const keys = Object.keys(inputHash);
const entries = results
.map((value, index) => [keys[index], value]);
return Object.fromEntries(entries);
});
};
Rất đơn giản phải không? Các phương thức prototype của đối tượng thực sự đơn giản hóa việc chuyển đổi giữa đối tượng và mảng cho chúng ta.
Củng Cố
Tôi sẽ tạm dừng ở đây một chút. Mặc dù điều này đáp ứng yêu cầu của chúng ta, nhưng có một sự khác biệt thời gian giữa khi chúng ta trích xuất các keys và values. Điều này có thể không gây ra vấn đề, nhưng nó có thể nếu chúng ta thay đổi đối tượng inputHash trước khi các promises được giải quyết.
Để ngăn chặn điều này, ngay cả từ mã có ý định xấu, chúng ta có thể sử dụng Object.entries() để trích xuất các cặp phù hợp chỉ một lần, đảm bảo chúng luôn khớp với nhau.
javascript
const promiseHash = (inputHash) => {
const entries = Object.entries(inputHash);
const keys = entries.map(([key]) => key);
const values = entries.map(([, value]) => value);
return Promise.all(values)
.then(results => Object.fromEntries(results
.map((result, index) => [keys[index], result])));
};
Chúng ta cũng có thể viết đoạn cuối này theo cách khác, tùy thuộc vào sự thoải mái của bạn khi đọc các phép toán hàm lồng nhau:
javascript
return Promise.all(values).then(results => {
const valueHash = results.map((result, index) => [
keys[index],
result,
]);
return Object.fromEntries(valueHash);
});
Ví Dụ
Một ví dụ đơn giản về thiết kế:
javascript
const hash = {
a: Promise.resolve(1),
b: new Promise((res) => setTimeout(res, 100, 2)),
}
promiseHash(hash).then(console.log)
// { a: 1, b: 2 }
Trường Hợp Sử Dụng
Thỉnh thoảng, tôi thấy một nhóm các yêu cầu tương tự trong Promise.all()...
javascript
const entitlements = Promise.all([
getUserPermissions(),
getPostPermissions(),
getModeratorPermissions(),
]).then(([ userPerms, postPerms, modPerms ]) => {
if (userPerms.canEdit) {
// ...
}
});
Mẫu này có thể rất giòn. Nó phụ thuộc vào việc nhà phát triển phải giữ hai mảng "không liên quan" đồng bộ với nhau. Nếu chúng ta thêm, xóa hoặc sắp xếp lại các mục, chúng ta có thể thay đổi hoặc làm hỏng ứng dụng một cách không mong muốn.
javascript
const entitlements = Promise.all([
getAdminPermissions(),
getUserPermissions(),
getPostPermissions(),
getModeratorPermissions(),
]).then(([ userPerms, postPerms, modPerms ]) => {
if (userPerms.canEdit) {
// ...
}
});
Hashes giúp ngăn chặn vấn đề này.
javascript
const entitlements = promiseHash({
admin: getAdminPermissions(),
user: getUserPermissions(),
post: getPostPermissions(),
mod: getModeratorPermissions(),
}).then((perms) => {
if (perms.user.canEdit) {
// ...
}
});
Biến Thể
Chúng ta cũng có thể sử dụng Promise.allSettled() dưới thiết kế này thay vì Promise.all().
javascript
const promiseSettledHash = (inputHash) => {
const entries = Object.entries(inputHash);
const keys = entries.map(([key]) => key);
const values = entries.map(([, value]) => value);
return Promise.allSettled(values)
.then(results => Object.fromEntries(results
.map((result, index) => [keys[index], result])));
};
Chúng ta có thể thậm chí làm điều này thành một tham số tùy chọn thứ hai để bạn có thể chọn cách nào xử lý hash của bạn.
javascript
const promiseHash = (inputHash, useSettled = false) => {
const entries = Object.entries(inputHash);
const keys = entries.map(([key]) => key);
const values = entries.map(([, value]) => value);
return (useSettled
? Promise.allSettled(values)
: Promise.all(values)
).then(results => Object.fromEntries(results
.map((result, index) => [keys[index], result])));
};
Lưu ý rằng chúng ta đã bao gồm tên đầy đủ trong mỗi cuộc gọi. Các phương thức Promise phải được gọi trên đối tượng Promise, vì vậy mã thay thế dưới đây sẽ gây ra lỗi.
javascript
(useSettled ? Promise.allSettled : Promise.all)(values)
// TypeError: Promise.all called on non-object
Kết Luận
Thực sự không có quá nhiều công việc để tạo ra một promise hash, nhưng nó có thể là một công cụ hữu ích nếu bạn thường xuyên phải thu thập một số promises. Có thể có những cách khác để thiết kế ứng dụng của bạn nhằm tránh những rủi ro từ Promise.all(), nhưng một wrapper đơn giản để cung cấp hỗ trợ hash có thể giúp mã của bạn dễ theo dõi hơn.