今天遇到一个场景,简单地说就是读取后端 token,但问题来了,有效的 token 需要满足两个条件(为了方便测试,时长和数量做了简化):
- token 通过接口获取,1000 毫秒内有效
- 接口一次性返回 3 个 token,每个 token 只能使用一次,用完需要再次调用接口
最简单的实现方式是写一个返回 Promise 的函数,在 Promise 中 resolve 出一个 token,但这个函数必然需要读取函数外部缓存的一些变量,用于在外部存在有效的 token 时直接返回一个 token,在与实际业务代码结合后,明显感觉不够优雅。
本文要介绍的是一种取值+控制逻辑更合理,但更晦涩的方法:generator + async/await。
- 首先我们要知道,generator 是个函数,作用就是让你在函数内描述怎么 yield 值。yield 值的次数可以是任意的(包括无限次),在每次 yield 值后挂起这个 generator 函数。yield 值可以形象地理解为「吐出一个值」。
async function* foo(){}定义的是一个 generator 函数,但在函数内可以使用 await 能力。
这两点结合起来,就是你可以在 generator 函数内封装出任意取值逻辑,而在外部还拥有灵活的取值时机与取值数量的控制逻辑。理解了这一点后,下面的代码对你来说就是稍显晦涩但逻辑控制更合理的了。
下面就是剥离业务后的代码,可以直接复制到浏览器控制台或者用 Node.js 环境执行:
/**
* 模拟接口
* @returns
*/
function fetchTokens() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const tokens = Array(3).fill(null).map(Math.random);
resolve(tokens);
}, 500);
});
}
/**
* token generator
*/
async function* tokenGenerator() {
// token 列表
let tokens = [];
// token 过期时长
const tokenDuration = 1000;
// token 过期时间点
let tokenOutOfDateTime = 0;
while (true) {
// token 耗尽
const isTokenOutOfBound = tokens.length <= 0;
// token 过期
const isTokenOutOfDate = Date.now() >= tokenOutOfDateTime;
if (isTokenOutOfBound || isTokenOutOfDate) {
tokens = await fetchTokens();
tokenOutOfDateTime = Date.now() + tokenDuration;
}
// 吐出一个 token
yield tokens.pop();
}
}
/**
* token 获取测试
*/
(async () => {
const generateToken = tokenGenerator();
let next;
// 一个个获取 token
next = await generateToken.next();
console.log(next.value);
next = await generateToken.next();
console.log(next.value);
// 等待 token 过期
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(true);
}, 1500);
});
next = await generateToken.next();
console.log(next.value);
next = await generateToken.next();
console.log(next.value);
next = await generateToken.next();
console.log(next.value);
// 一次获取任意多个 token
const nextList = await Promise.all([
generateToken.next(),
generateToken.next(),
generateToken.next(),
generateToken.next(),
generateToken.next(),
]);
const tokens = nextList.map((nextItem) => nextItem.value);
console.log(tokens);
})();