JavaScript 中使用 Generator 的高级技巧:批处理、取消与暂停

作者:木棉

时间:2026-10-22

浏览:447

本文深入探讨 JavaScript 中 Generator 函数的实际应用,超越基础的 async/await。通过自定义执行器,展示如何利用 yield 关键字实现数据批处理以优化性能、支持请求取消以及提供暂停/恢复渲染的功能。文章提供了具体的代码示例,解析如何在不修改业务逻辑的前提下,通过 Generator 控制异步执行流程,解决复杂场景下的性能与交互问题。

文章目录

Generator 概述与现状

Generator 是一种功能强大的语法特性,但在实际开发中使用频率并不高(参考相关调查数据)。其普及度低于 async/await 的主要原因在于使用复杂度较高,且调试体验往往不如预期,很多时候甚至需要回到传统的回调模式。尽管可以通过封装获得类似 async/await 的简洁体验,但开发者通常仍倾向于选择更直观的 async/await。

Generator 概述与现状 

然而,Generator 的核心优势在于允许开发者通过 yield 关键字自定义代码遍历逻辑。这是一种极具威力的语法,因为它允许我们直接操控代码的执行流程。从基础的同步操作开始,我们可以逐步探索更高级的功能,例如取消操作等。

文中涉及的功能示例代码已开源至 github.com/Bloomca/obs…,供读者参考。

批处理优化

批处理 (或计划)

执行 Generator 函数会返回一个遍历器对象,这意味着我们可以通过该对象同步地遍历执行过程。这样做的一个典型应用场景是实现批处理。假设我们需要下载 1000 个项目并在表格中逐行显示(此处假设不使用现代框架)。虽然立即展示所有数据看似无害,但在性能受限的设备(如普通电脑或手机)上,这可能导致界面卡顿。因此,我们需要通过某种机制来延迟执行,以优化用户体验。

需要注意的是,此示例旨在展示性能优化技巧。在遇到实际性能瓶颈之前,不建议过度设计,因为过早优化往往是问题的根源。


// 最初的同步实现版本
function renderItems(items) {
 for (item of items) {
 renderItem(item);
 }
}
// 函数将由我们的执行器遍历执行
// 实际上,我们可以用相同的同步方式来执行它!
function* renderItems(items) {
 // 我使用 for..of 遍历方法来避免新函数的产生
 for (item of items) {
 yield renderItem(item);
 }
}

代码看似没有本质区别,但关键差异在于:现在我们可以不修改源代码的情况下,以不同的方式运行该函数。正如前文所述,我们无需等待,而是可以同步执行。例如,在每个 yield 后添加 4 毫秒(JavaScript VM 的一个心跳周期)的延迟。如果有 1000 个项目,渲染总耗时约为 4 秒。若希望将渲染时间缩短至 2 秒,直观的想法是每次渲染 2 个项目。如果使用 Promise 方案,实现将变得复杂,需要额外传递每次渲染的项目数量参数。而通过 Generator 执行器,虽然仍需传递该参数,但 renderItems 方法本身完全不受影响。


function runWithBatch(chunk, fn, ...args) {
 const gen = fn(...args);
 let num = 0;
 return new Promise((resolve, promiseReject) => {
 callNextStep();
 function callNextStep(res) {
  let result;
  try {
  result = gen.next(res);
  } catch (e) {
  return reject(e);
  }
  next(result);
 }
 function next({ done, value }) {
  if (done) {
  return resolve(value);
  }
  // every chunk we sleep for a tick
  if (num++ % chunk === 0) {
  return sleep(4).then(proceed);
  } else {
  return proceed();
  }
  function proceed() {
  return callNextStep(value);
  }
 }
 });
}
// 第一个参数 —— 每批处理多少个项目
const items = [...];
batchRunner(2, function*() {
 for (item of items) {
 yield renderItem(item);
 }
});

由此可见,我们可以轻松调整每批处理的项目数量,甚至切换回正常的同步执行方式,所有这些操作均不影响 renderItems 方法的定义。

请求取消机制

取消

接下来考虑传统功能——请求取消。在之前的文章《如何取消你的 Promise?》中已详细讨论过此话题,此处将引用部分代码进行说明。


function runWithCancel(fn, ...args) {
 const gen = fn(...args);
 let cancelled, cancel;
 const promise = new Promise((resolve, promiseReject) => {
 // define cancel function to return it from our fn
 // 定义 cancel 方法,并返回它
 cancel = () => {
  cancelled = true;
  reject({ reason: 'cancelled' });
 };
 onFulfilled();
 function onFulfilled(res) {
  if (!cancelled) {
  let result;
  try {
   result = gen.next(res);
  } catch (e) {
   return reject(e);
  }
  next(result);
  return null;
  }
 }
 function onRejected(err) {
  var result;
  try {
  result = gen.throw(err);
  } catch (e) {
  return reject(e);
  }
  next(result);
 }
 function next({ done, value }) {
  if (done) {
  return resolve(value);
  }
  // 假设我们总是接收 Promise,所以不需要检查类型
  return value.then(onFulfilled, onRejected);
 }
 });
 return { promise, cancel };
}

此方案的最大优势在于,我们可以取消所有尚未执行的请求(甚至可以向执行器传递类似 AbortController 的对象参数,从而取消当前正在进行的请求)。关键在于,我们无需修改业务逻辑中的任何一行代码即可实现这一功能。

暂停与恢复功能

暂停/恢复

另一个特殊需求是暂停与恢复功能。应用场景例如:渲染 1000 行数据速度较慢,我们希望为用户提供暂停/恢复渲染的功能,以便用户可以停止后台工作,专心阅读已下载的内容。


// 实现渲染的方法还是一样的
function* renderItems() {
 for (item of items) {
 yield renderItem(item);
 }
}
function runWithPause(genFn, ...args) {
 let pausePromiseResolve = null;
 let pausePromise;
 const gen = genFn(...args);
 const promise = new Promise((resolve, reject) => {
 onFulfilledWithPromise();
 function onFulfilledWithPromise(res) {
  if (pausePromise) {
  pausePromise.then(() => onFulfilled(res));
  } else {
  onFulfilled(res);
  }
 }
 function onFulfilled(res) {
  let result;
  try {
  result = gen.next(res);
  } catch (e) {
  return reject(e);
  }
  next(result);
  return null;
 }
 function onRejected(err) {
  var result;
  try {
  result = gen.throw(err);
  } catch (e) {
  return reject(e);
  }
  next(result);
 }
 function next({ done, value }) {
  if (done) {
  return resolve(value);
  }
  // 假设我们总是接收 Promise,所以不需要检查类型
  return value.then(onFulfilledWithPromise, onRejected);
 }
 });
 return {
 pause: () => {
  pausePromise = new Promise(resolve => {
  pausePromiseResolve = resolve;
  });
 },
 resume: () => {
  pausePromiseResolve();
  pausePromise = null;
 },
 promise
 };
}

调用该执行器后,我们将获得一个具有暂停/恢复功能的对象。这一切均基于我们之前的业务代码实现。因此,如果存在大量“沉重”的请求链且耗时较长,而你又希望为用户提供暂停/恢复功能,只需在代码中实现相应的执行器即可。

错误处理与重试策略

错误处理

这里涉及一个神秘的 onRejected 调用,这是本部分讨论的重点。如果使用常规的 async/await 或 Promise 链式写法,错误处理通常依赖 try/catch 语句,这往往需要添加大量逻辑代码。通常情况下,若需以特定方式(如重试)处理错误,我们只能在 Promise 内部进行处理,这可能导致回调自己,甚至回到同一点。这并非通用解决方案——遗憾的是,Generator 在此处也有限制:虽然我们可以控制执行流程,但无法移动 Generator 函数的主体,即无法后退一步重新执行命令。一个可行的解决方案是使用命令模式(command pattern),即 yield 的结果数据结构应包含执行该命令所需的所有信息,以便我们能够重新执行它。因此,我们的方法需要调整为:


function* renderItems() {
 for (item of items) {
 // 我们需要将所有东西传递出去:
 // 方法, 内容, 参数
 yield [renderItem, null, item];
 }
}

正如所见,这种写法可能使代码意图变得晦涩。因此,或许更好的做法是编写一个 wrapWithRetry 方法,该方法检查 catch 代码块中的错误类型并尝试重新执行。但我们仍可以做些不影响功能的事情,例如增加忽略错误的策略。在 async/await 中,我们不得不使用 try/catch 包装每个调用,或者添加空的 .catch(() => {}) 部分。而使用 Generator,我们可以编写一个忽略所有错误的执行器。


function runWithIgnore(fn, ...args) {
 const gen = fn(...args);
 return new Promise((resolve, promiseReject) => {
 onFulfilled();
 function onFulfilled(res) {
  proceed({ data: res });
 }
 // 这些是 yield 返回的错误
 // 我们想忽略它们
 // 所以我们像往常一样做,但不去传递出错误
 function onRejected(error) {
  proceed({ error });
 }
 function proceed(data) {
  let result;
  try {
  result = gen.next(data);
  } catch (e) {
  // 这些错误是同步错误(比如 TypeError 等)
  return reject(e);
  }
  // 为了区分错误和正常的结果
  // 我们用它来执行
  next(result);
 }
 function next({ done, value }) {
  if (done) {
  return resolve(value);
  }
  // 假设我们总是接收 Promise,所以不需要检查类型
  return value.then(onFulfilled, onRejected);
 }
 });
}

Generator 与 async/await 的对比

关于 async/await

Async/await 是目前的首选语法(甚至 co 库也提及了它),也是未来的方向。然而,Generator 仍包含在 ECMAScript 标准中,这意味着使用它们无需额外依赖,只需编写几个工具函数即可。本文试图展示一些较为复杂的示例,其价值取决于具体应用场景。请记住,熟悉 Generator 的开发者并不多,如果整个代码库中仅有一处使用它们,使用 Promise 可能更为简单。但另一方面,通过 Generator,某些问题可以被优雅且简洁地解决。

总结

以上介绍了在 JavaScript 中使用 Generator 的高级方法,希望对大家有所帮助。如有疑问,欢迎留言,小编会及时回复。感谢大家对本站的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章