generator + async/await 结合起来能做什么?

652 阅读2分钟

今天遇到一个场景,简单地说就是读取后端 token,但问题来了,有效的 token 需要满足两个条件(为了方便测试,时长和数量做了简化):

  1. token 通过接口获取,1000 毫秒内有效
  2. 接口一次性返回 3 个 token,每个 token 只能使用一次,用完需要再次调用接口

最简单的实现方式是写一个返回 Promise 的函数,在 Promise 中 resolve 出一个 token,但这个函数必然需要读取函数外部缓存的一些变量,用于在外部存在有效的 token 时直接返回一个 token,在与实际业务代码结合后,明显感觉不够优雅。

本文要介绍的是一种取值+控制逻辑更合理,但更晦涩的方法:generator + async/await。

  1. 首先我们要知道,generator 是个函数,作用就是让你在函数内描述怎么 yield 值。yield 值的次数可以是任意的(包括无限次),在每次 yield 值后挂起这个 generator 函数。yield 值可以形象地理解为「吐出一个值」。
  2. 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);
})();