JS `Generator` 函数用于异步流控制:实现类 `async/await` 行为

各位观众,欢迎来到“老司机带你飞:JS Generator 异步流控制骚操作”讲座!今天咱们不飙车,改玩“发电机”,看看怎么用这玩意儿模拟 async/await,把异步代码安排得明明白白。

开场白:异步的烦恼

话说前端er,谁还没被异步回调虐过?回调地狱,promise链式调用,各种then、catch,写得头昏眼花。后来 async/await 横空出世,拯救了万千程序员于水火之中,让异步代码看起来像同步一样,简直不要太爽!

但是,如果回到 async/await 还没普及的年代,或者你想深入了解 JS 异步的底层机制,Generator 函数就是你的秘密武器! 它就像一个暂停开关,让你的函数可以“走走停停”,控制异步流程,实现类似 async/await 的效果。

第一章:Generator 函数初体验

首先,咱们来认识一下 Generator 函数。它长得很像普通函数,但有几个关键的不同:

  1. 函数声明时,在 function 关键字后面加个星号 *
  2. 函数内部可以使用 yield 关键字,用来暂停函数的执行,并返回一个值。
function* myGenerator() {
  console.log("函数开始执行");
  yield "第一步";
  console.log("第一步执行完毕");
  yield "第二步";
  console.log("第二步执行完毕");
  return "函数执行结束";
}

const gen = myGenerator(); // 注意:调用 Generator 函数不会立即执行,而是返回一个迭代器对象

console.log(gen.next()); // { value: "第一步", done: false }
console.log(gen.next()); // { value: "第二步", done: false }
console.log(gen.next()); // { value: "函数执行结束", done: true }
console.log(gen.next()); // { value: undefined, done: true }

解读一下:

  • myGenerator() 函数声明了一个 Generator
  • 调用 myGenerator() 并没有立即执行函数体内的代码,而是返回一个迭代器对象 gen
  • gen.next() 方法用来启动或恢复 Generator 函数的执行。
  • 每次调用 next(),函数会执行到下一个 yield 表达式,然后暂停,并返回一个对象,包含两个属性:
    • value: yield 表达式后面的值。
    • done: 表示 Generator 函数是否执行完毕。false 表示未完成,true 表示已完成。
  • Generator 函数执行到 return 语句或函数体结束时,done 变为 truevalue 则是 return 语句后面的值,如果没有 return,则 valueundefined
  • 后续再调用 next()value 始终是 undefineddone 始终是 true

第二章:Generator 的传参与控制

Generator 函数的强大之处在于,它可以接受参数,并且可以通过 next() 方法向 yield 表达式传递值,实现更灵活的控制。

function* myGenerator(initialValue) {
  console.log("初始值:", initialValue);

  const firstValue = yield "请输入第一个值";
  console.log("接收到的第一个值:", firstValue);

  const secondValue = yield "请输入第二个值";
  console.log("接收到的第二个值:", secondValue);

  return firstValue + secondValue;
}

const gen = myGenerator(10);

console.log(gen.next()); // { value: "请输入第一个值", done: false }
console.log(gen.next(20)); // { value: "请输入第二个值", done: false }
console.log(gen.next(30)); // { value: 50, done: true }

分析:

  • myGenerator(10) 接收一个初始值 10
  • 第一次 gen.next() 启动 Generator 函数,执行到第一个 yield,返回提示信息。
  • 第二次 gen.next(20)20 作为第一个 yield 表达式的返回值,赋值给 firstValue
  • 第三次 gen.next(30)30 作为第二个 yield 表达式的返回值,赋值给 secondValue
  • 最后,函数返回 firstValue + secondValue 的结果 50

第三章:Generator 与异步操作的完美结合

现在,重头戏来了!咱们看看怎么用 Generator 函数来处理异步操作,模拟 async/await 的效果。

假设我们有一个模拟异步请求的函数 fetchData

function fetchData(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = `从 ${url} 获取的数据`;
      resolve(data);
    }, 1000); // 模拟 1 秒的延迟
  });
}

现在,我们用 Generator 函数来依次请求两个 URL,并处理返回的数据:

function* fetchDataFlow() {
  try {
    const data1 = yield fetchData("URL1");
    console.log("从 URL1 获取的数据:", data1);

    const data2 = yield fetchData("URL2");
    console.log("从 URL2 获取的数据:", data2);

    return "所有数据获取完毕";
  } catch (error) {
    console.error("发生错误:", error);
  }
}

重点: yield fetchData("URL1") 这里,fetchData 返回的是一个 Promise 对象。我们需要一种机制,能够自动执行这个 Promise,并将 Promiseresolve 值传递给 data1

第四章:手动实现一个 runGenerator 函数

为了简化 Generator 函数的执行,我们可以手动实现一个 runGenerator 函数,它能够自动执行 Generator 函数,并处理 Promise

function runGenerator(generatorFunction) {
  return new Promise((resolve, reject) => {
    const iterator = generatorFunction(); // 创建迭代器

    function step(nextResult) {
      try {
        const next = iterator.next(nextResult); // 执行 Generator 函数

        if (next.done) {
          // Generator 函数执行完毕
          resolve(next.value);
        } else {
          // Generator 函数还未执行完毕
          Promise.resolve(next.value) // 确保 value 是一个 Promise 对象
            .then(
              (value) => {
                step(value); // 递归调用 step,将 Promise 的 resolve 值传递给下一个 yield 表达式
              },
              (err) => {
                iterator.throw(err); // 处理 Promise 的 reject 情况
                reject(err);
              }
            );
        }
      } catch (error) {
        reject(error);
      }
    }

    step(undefined); // 启动 Generator 函数
  });
}

代码解读:

  1. runGenerator 函数接收一个 Generator 函数作为参数,并返回一个 Promise 对象。
  2. runGenerator 内部,首先创建一个迭代器对象 iterator
  3. step 函数负责执行 Generator 函数的每一步:
    • 调用 iterator.next(nextResult) 执行 Generator 函数,并将上次 Promiseresolve 值作为参数传递给 yield 表达式。
    • 如果 next.donetrue,表示 Generator 函数执行完毕,resolve 整个 Promise
    • 如果 next.donefalse,表示 Generator 函数还未执行完毕,则判断 next.value 是否为 Promise
      • 如果是 Promise,则使用 Promise.resolve(next.value).then(...) 确保它是一个 Promise 对象,然后等待 Promise 完成,并将 resolve 的值传递给 step 函数,进行下一次递归调用。
      • 如果 Promise 失败,则调用 iterator.throw(err) 将错误抛给 Generator 函数,并 reject 整个 Promise
  4. 最后,调用 step(undefined) 启动 Generator 函数。

第五章:使用 runGenerator 函数

现在,我们可以使用 runGenerator 函数来执行我们的 fetchDataFlow 函数:

runGenerator(fetchDataFlow)
  .then((result) => {
    console.log("最终结果:", result);
  })
  .catch((error) => {
    console.error("发生错误:", error);
  });

这样,fetchDataFlow 函数就会自动执行,依次请求 URL1URL2,并输出结果。 是不是有点 async/await 的味道了?

第六章:错误处理

Generator 函数还可以通过 try...catch 语句来捕获异步操作中的错误。 在 fetchDataFlow 函数中,我们已经加入了 try...catch 语句。 如果 fetchData 函数抛出错误,catch 块将会捕获这个错误,并进行处理。 runGenerator 函数中的 iterator.throw(err) 会将错误抛给 Generator 函数,让 Generator 函数有机会处理这个错误。

第七章:总结与展望

通过 Generator 函数和 runGenerator 函数,我们成功地模拟了 async/await 的异步流程控制。 虽然手写 runGenerator 函数比较麻烦,但是它可以帮助我们更深入地理解 async/await 的底层机制。

特性 async/await Generator + runGenerator
语法 更简洁、更易读 略显繁琐
错误处理 try…catch try…catch
兼容性 需要 ES2017+ 支持 兼容性更好
底层实现 基于 Promise 和 Generator 手动实现

虽然现在 async/await 已经成为主流,但 Generator 函数仍然有它的用武之地,例如:

  • 状态管理: 可以用 Generator 函数来管理复杂的状态,例如 Redux Saga。
  • 游戏开发: 可以用 Generator 函数来控制游戏角色的行为,实现复杂的 AI 逻辑。
  • 数据流处理: 可以用 Generator 函数来处理大量的数据流,例如读取文件、处理网络数据等。

第八章:更进一步:co 库

实际上,有一个流行的库叫做 co,它封装了 runGenerator 函数,提供了更方便的 Generator 函数执行方式。 co 库的实现原理和我们手写的 runGenerator 函数类似,但是它做了更多的优化和增强,例如支持并行执行、超时处理等。

const co = require('co');

co(function* () {
  const data1 = yield fetchData("URL1");
  console.log("从 URL1 获取的数据:", data1);

  const data2 = yield fetchData("URL2");
  console.log("从 URL2 获取的数据:", data2);

  return "所有数据获取完毕";
}).then((result) => {
  console.log("最终结果:", result);
}).catch((error) => {
  console.error("发生错误:", error);
});

使用 co 库,我们可以更简洁地执行 Generator 函数,而无需手动编写 runGenerator 函数。

结束语:

好了,今天的“JS Generator 异步流控制骚操作”讲座就到这里。 希望通过今天的讲解,大家对 Generator 函数有了更深入的了解。 虽然 async/await 很香,但是了解 Generator 的底层机制,可以帮助我们更好地理解 JS 异步编程的本质。 记住,熟练掌握 Generator 函数,你也能成为异步编程的老司机! 感谢大家的观看,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注