Promise 使用技巧
单例 Promise
有些时候我们在进行某种操作之前需要先进行初始化,初始化完成之后才能进行操作。比如在查询数据库之前,需要先连接到数据库。此时,若是并行执行多个查询,可能会多次连接数据库,导致出现问题。
class DbClient {
private isConnected: boolean;
constructor() {
this.isConnected = false;
}
private async connect() {
if (this.isConnected) {
return;
}
await connectToDatabase(); // stub
this.isConnected = true;
}
public async getRecord(recordId: string) {
await this.connect();
return getRecordFromDatabase(recordId); // stub
}
}
// 并发查询
const db = new DbClient();
const [record1, record2] = await Promise.all([
db.getRecord('record1'),
db.getRecord('record2'),
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
使用单例 Promise 可以解决这个问题。
class DbClient {
private connectionPromise: Promise<void> | null;
constructor() {
this.connectionPromise = null;
}
private async connect() {
if (!this.connectionPromise) {
this.connectionPromise = connectToDatabase(); // stub
}
return this.connectionPromise;
}
public async getRecord(recordId: string) {
await this.connect();
return getRecordFromDatabase(recordId); // stub
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Promise 缓存
当我们去调用基于 Promise 的异步方法,并且想要将每次调用的结果缓存起来方便以后使用,要怎么处理呢?比如请求获取用户信息的 API:
const getUserById = async (userId: string): Promise<User> => {
const user = await request.get(`https://users-service/${userId}`);
return user;
};
1
2
3
4
2
3
4
简单的解决方案:
const usersCache = new Map<string, User>();
const getUserById = async (userId: string): Promise<User> => {
if (!usersCache.has(userId)) {
const user = await request.get(`https://users-service/${userId}`);
usersCache.set(userId, user);
}
return usersCache.get(userId);
};
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
但是这种实现,是在调用完成之后分配缓存,无法解决并发场景的问题:
await Promise.all([
getUserById('user1'),
getUserById('user1')
]);
1
2
3
4
2
3
4
因此我们不能缓存调用结果,而是应该缓存调用 API 时返回的 Promise。
const userPromisesCache = new Map<string, Promise<User>>();
const getUserById = (userId: string): Promise<User> => {
if (!userPromisesCache.has(userId)) {
const userPromise = request.get(`https://users-service/v1/${userId}`);
userPromisesCache.set(userId, userPromise);
}
return userPromisesCache.get(userId)!;
};
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
此时,一旦 API 请求创建了,我们就将该请求的 Promise 缓存起来,在并发场景下再次请求时,返回的就是第一次请求缓存的 Promise。
lodash.memoize 实现
使用lodash.memoizeopen in new window可以将上面最后一个解决方案简化为:
import _ from 'lodash';
const getUserById = _.memoize(async (userId: string): Promise<User> => {
const user = await request.get(`https://users-service/${userId}`);
return user;
});
1
2
3
4
5
6
2
3
4
5
6
lodash.memoize
会将缓存函数的返回缓存下来,而async
函数的返回,正好是个 Promise。
错误处理
对于 API 客户端,我们应该考虑操作可能失败的可能性。如果我们的内存实现已缓存了被拒绝的 Promise ,则所有将来的调用都将以同样的失败 Promise 被拒绝!
幸运的是,memoizeeopen in new window库支持仅缓存resolved
的 Promiseopen in new window。我们的最后一个示例变为:
import memoize from 'memoizee';
const getUserById = memoize(async (userId: string): Promise<User> => {
const user = await request.get(`https://users-service/${userId}`);
return user;
}, { promise: true});
1
2
3
4
5
6
2
3
4
5
6
Reference
- Advanced Async Patterns: Singleton Promisesopen in new window
- Advanced Promise Patterns: Promise Memoizationopen in new window