1. 为什么“写个for循环”不再是万能解药:从一个真实内存溢出事故说起

上周五下午三点,监控告警突然炸了——某内部数据导出服务的Node.js进程连续三次OOM(Out of Memory)崩溃,错误日志里清一色是 FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory 。运维同事甩来一段复现代码,核心逻辑只有三行:

const allRecords = await db.query('SELECT * FROM huge_table'); // 返回80万条JSON对象
return allRecords.map(record => transform(record)); // 转换字段、拼接字符串、生成HTML片段

问题很直观:80万条记录一次性加载进内存,每条记录平均占2KB,光原始数据就吃掉1.6GB;再叠加转换过程中的临时字符串、闭包引用、V8引擎的内部开销……内存墙瞬间被撞穿。

但真正让我停下手头工作的是团队群里的一句吐槽:“要是Java有Stream就好了,能一行行处理。”——这句话像根针扎醒了我。JavaScript真没有类似能力?当然有。只是我们大多数人,包括我自己,在过去五年里写的90%的前端和Node.js脚本,都下意识地绕开了它: Generator(生成器)

这不是一个新概念。ECMAScript 2015(ES6)就已正式落地,距今快十年了。可翻看公司前端代码库, yield 关键字出现次数为零;Node.js后端服务里, function* 的使用率比TypeScript里的 unknown 类型还稀有。它不像Promise那样被框架强制封装,也不像async/await那样有语法糖加持,它安静得像个被遗忘的工具箱底层螺丝钉——直到你真的需要拧紧那颗即将崩飞的螺栓。

“Grundlegendes zu Generatoren in JavaScript”(JavaScript中生成器的基础知识)这个德语标题,表面看是入门教程,实则直指一个被严重低估的工程现实: 当数据规模突破单次内存承载阈值时,Generator不是“可选项”,而是“必选项”。 它解决的从来不是“怎么写更酷的代码”,而是“怎么让代码在真实生产环境里不崩溃”。

你不需要立刻重构整个系统。但当你下次面对Excel导入、大文件解析、实时日志流处理、甚至只是给百万级用户列表加个分页懒加载时,请记住:Generator提供的不是语法糖,而是一种 内存可控的数据流建模能力 。它把“一次性全量加载”的暴力模式,切换成“按需索取、即用即弃”的呼吸式处理。这种范式转变,恰恰是现代JavaScript工程化绕不开的底层认知升级。

2. Generator不是函数,是状态机:拆解yield背后的真实执行模型

很多教程说“Generator函数用 function* 声明,用 yield 暂停”,这没错,但过于表象。真正理解Generator,必须穿透语法,看到V8引擎里它到底在做什么。我用一个最简例子,带你看清它的骨骼:

function* counter() {
  console.log('Step 1: start');
  yield 1;
  console.log('Step 2: after first yield');
  yield 2;
  console.log('Step 3: after second yield');
  return 3;
}

关键不在 yield 本身,而在 调用 counter() 时发生了什么 。执行这行代码:

const gen = counter();
console.log(gen); // 输出什么?

你得到的绝不是一个数字,也不是一个普通函数返回值。 gen 是一个 Generator对象实例 ,它内部封装了完整的执行上下文(execution context),包括:

  • 当前指令指针(指向哪行代码)
  • 局部变量栈( i 的值、 console.log 的参数等)
  • 内部状态( suspended executing closed

这才是Generator的本质: 一个可暂停、可恢复、自带状态快照的协程(Coroutine)实例。 yield 不是“返回值并结束”,而是“保存当前所有现场,交出控制权,等待下次唤醒”。

我们用 next() 方法一步步唤醒它:

const gen = counter();

// 第一次调用
const result1 = gen.next();
console.log(result1); 
// { value: 1, done: false }
// 同时控制台输出:'Step 1: start'

// 第二次调用
const result2 = gen.next();
console.log(result2);
// { value: 2, done: false }
// 同时控制台输出:'Step 2: after first yield'

// 第三次调用
const result3 = gen.next();
console.log(result3);
// { value: 3, done: true }
// 同时控制台输出:'Step 3: after second yield'

注意三个细节:

  1. value 字段的来源 :第一次 yield 1 value 就是1;第二次 yield 2 value 就是2;第三次 return 3 value 才是3。 return 语句的值,只在Generator彻底结束( done: true )时才成为 value
  2. done 字段的语义 false 表示Generator还有后续步骤可执行; true 表示已执行完毕,再调用 next() 会永远返回 { value: undefined, done: true }
  3. 控制流的精确性 yield 之后的代码(如 console.log('Step 2...') 不会在 yield 执行时立即运行 ,而是在下一次 next() 调用时,从 yield 语句之后的第一行开始执行。这是状态机的核心特征——暂停点精准锚定在 yield 处。

提示:你可以把Generator对象想象成一台老式胶片放映机。 next() 是扳动手柄的动作,每次扳动,胶片前进一格(执行到下一个 yield ),银幕上显示当前帧( value )。 done: true 意味着胶片已到尽头,再扳手柄也只会看到黑屏( undefined )。

这种状态机模型,直接决定了Generator的不可替代性: 它让JavaScript拥有了原生的、无栈溢出风险的“惰性求值”能力。 普通函数一旦执行,就必须跑完所有代码或抛出异常;而Generator可以像呼吸一样,吸( next() )一口,吐( yield )一点,中间随时暂停,内存只保留当前帧所需的数据。

3. 从“数组遍历”到“无限序列”:Generator如何重塑数据消费范式

理解了Generator是状态机,下一步要破除一个根深蒂固的误解: Generator不是用来“替代for循环”的,而是用来“定义新的数据源”的。 这个认知跃迁,是掌握其威力的关键。

3.1 传统数组遍历的硬伤:内存与逻辑的强耦合

假设我们要处理一个包含100万个数字的数组:

const numbers = Array.from({length: 1000000}, (_, i) => i + 1);

// 方案A:for循环(内存友好,但逻辑分散)
for (let i = 0; i < numbers.length; i++) {
  const n = numbers[i];
  if (n % 2 === 0 && n > 1000) {
    processEvenAndLarge(n);
  }
}

// 方案B:函数式链式调用(逻辑清晰,但内存爆炸)
numbers
  .filter(n => n % 2 === 0)
  .filter(n => n > 1000)
  .map(n => n * 2)
  .forEach(process);

方案A的问题在于:业务逻辑( processEvenAndLarge )和遍历逻辑( for 循环)混在一起,复用性差;方案B的问题更致命—— .filter() .map() 会创建全新的中间数组。100万个数字,经过两次 filter ,可能产生两个各50万元素的数组,内存占用瞬间翻倍。这就是“内存与逻辑强耦合”的代价:你想用声明式语法表达意图,却被迫为每一次操作支付内存副本的开销。

3.2 Generator的解法:将“数据源”与“处理逻辑”彻底解耦

Generator让我们能定义一个 按需生成数据的源头 ,而非一次性加载全部数据的容器。看这个经典实现:

// 定义一个无限自然数生成器(内存恒定!)
function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++;
  }
}

// 定义一个偶数过滤器(也是Generator!)
function* filterEven(gen) {
  for (const n of gen) {
    if (n % 2 === 0) yield n;
  }
}

// 定义一个大于阈值的过滤器
function* filterGreaterThan(gen, threshold) {
  for (const n of gen) {
    if (n > threshold) yield n;
  }
}

// 定义一个乘法映射器
function* multiplyBy(gen, factor) {
  for (const n of gen) {
    yield n * factor;
  }
}

现在,组合它们:

const pipeline = multiplyBy(
  filterGreaterThan(
    filterEven(naturalNumbers()), 
    1000
  ), 
  2
);

// 消费前10个结果(只计算10次,内存只存当前值!)
for (let i = 0; i < 10; i++) {
  const { value, done } = pipeline.next();
  if (done) break;
  console.log(value); // 输出:2004, 2008, 2012, ... (前10个偶数且>1000,再乘2)
}

这里发生了什么革命性变化?

  • 内存恒定 naturalNumbers() 不生成任何数组,只维护一个计数器 n (几个字节);每个 filter multiplyBy 也只保存当前迭代的 n 值。处理100万个数字,内存占用和处理10个数字完全一样。
  • 逻辑解耦 filterEven filterGreaterThan multiplyBy 是纯函数式的、可复用的“管道算子”(pipe operators)。它们不关心数据从哪来(数组?API流?文件读取?),只关心“对当前这个值做什么”。
  • 惰性求值 pipeline.next() 被调用时,才触发 multiplyBy for...of ,进而触发 filterGreaterThan for...of ,再触发 filterEven for...of ,最终驱动 naturalNumbers yield 。整个链条像多米诺骨牌,只推倒第一张,后续自动连锁反应。

注意: for...of 循环是Generator的天然搭档。它内部会自动调用 next() ,并优雅处理 done: true 。所以 for (const n of pipeline) 比手动 next() 更简洁安全。

这种“数据源 → 管道算子 → 消费者”的三层架构,正是现代流式处理(如RxJS、Node.js Streams)的思想源头。Generator是JavaScript语言层面对这一范式的原生支持,无需引入任何第三方库。

4. 生产级实战:用Generator重构大文件解析与API分页请求

理论终须落地。我以两个高频生产场景为例,展示Generator如何从“玩具概念”变成“救命稻草”。

4.1 场景一:解析GB级CSV文件(Node.js环境)

需求:上传一个2GB的销售订单CSV文件,逐行解析,校验格式,将有效订单写入数据库,跳过无效行,并实时返回处理进度。

传统做法(灾难性):

// ❌ 危险!readFileSync会把2GB文件全读进内存
const fileContent = fs.readFileSync('orders.csv', 'utf8');
const lines = fileContent.split('\n');
lines.forEach(line => {
  const order = parseLine(line);
  if (isValid(order)) db.insert(order);
});

Generator解法(内存可控):

const fs = require('fs');
const readline = require('readline');

// 核心:将文件流转化为Generator
function* csvLineGenerator(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  // 关键:用async iterator包装readline
  // (注意:Node.js 12+原生支持for await...of on ReadableStream)
  for await (const line of rl) {
    yield line;
  }
  fileStream.close();
}

// 业务逻辑:可复用的解析管道
function* parseOrders(lines) {
  for (const line of lines) {
    try {
      const [id, amount, date] = line.split(',');
      yield { id: parseInt(id), amount: parseFloat(amount), date };
    } catch (e) {
      console.warn(`Invalid line skipped: ${line}`);
      continue;
    }
  }
}

function* validateOrders(orders) {
  for (const order of orders) {
    if (order.id > 0 && order.amount > 0 && isValidDate(order.date)) {
      yield order;
    }
  }
}

// 执行:按需消费,内存恒定
async function processBigCSV(filePath) {
  const lines = csvLineGenerator(filePath);
  const parsed = parseOrders(lines);
  const valid = validateOrders(parsed);

  let count = 0;
  for await (const order of valid) { // 注意:这里用for await...of,因为csvLineGenerator返回的是AsyncIterator
    await db.insert(order);
    count++;
    if (count % 1000 === 0) {
      console.log(`Processed ${count} orders...`);
      updateProgress(count); // 实时更新UI进度条
    }
  }
  console.log(`Total processed: ${count}`);
}

为什么这能工作? 因为 readline.createInterface 返回的是一个 ReadableStream ,而Node.js 12+的 for await...of 语法糖,会自动调用其 [Symbol.asyncIterator]() 方法,该方法返回一个 AsyncIterator 对象。 AsyncIterator next() 方法返回 Promise<{value, done}> ,完美适配异步I/O。Generator在这里扮演了“同步逻辑粘合剂”的角色,把异步的流式读取,包装成同步的、可组合的 for...of 消费接口。

4.2 场景二:处理分页API的海量数据(浏览器环境)

需求:调用一个分页API( /api/users?page=1&size=100 ),获取全部10万用户数据,进行前端聚合分析(如统计地域分布),不阻塞UI。

传统做法(繁琐且易错):

// ❌ 手动管理page、递归、错误重试、状态跟踪...
let allUsers = [];
let page = 1;
const fetchAll = async () => {
  while (true) {
    const res = await fetch(`/api/users?page=${page}&size=100`);
    const data = await res.json();
    allUsers.push(...data.users);
    if (data.total <= page * 100) break;
    page++;
  }
  return allUsers; // 10万用户对象全在内存里!
};

Generator解法(声明式、可中断、内存友好):

// 封装分页API为Generator
async function* userPageGenerator() {
  let page = 1;
  while (true) {
    try {
      const res = await fetch(`/api/users?page=${page}&size=100`);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);

      const data = await res.json();
      // yield整个页面数据(100个用户),而非单个用户,平衡粒度与开销
      yield data.users;

      // 检查是否还有下一页
      if (data.users.length < 100 || data.total <= page * 100) {
        break;
      }
      page++;
    } catch (error) {
      console.error(`Failed to fetch page ${page}:`, error);
      // 可在此加入指数退避重试逻辑
      throw error; // 或者 yield [] 继续
    }
  }
}

// 前端聚合分析(内存友好)
async function analyzeUserDistribution() {
  const distribution = new Map(); // 地域 -> 用户数

  // 按页消费,每页处理完即可释放内存
  for await (const users of userPageGenerator()) {
    for (const user of users) {
      const region = user.region || 'unknown';
      distribution.set(region, (distribution.get(region) || 0) + 1);
    }
    // 更新UI:显示当前已处理页数和总用户数
    updateUI({ currentPage: users.length > 0 ? 'processing' : 'done', totalUsers: [...distribution.values()].reduce((a,b)=>a+b,0) });
  }

  return Object.fromEntries(distribution);
}

关键优势:

  • 内存友好 :同一时刻,内存中只存在当前页的100个用户对象,而非10万个。
  • 可中断 :用户点击“取消”按钮时,可直接 break 循环,Generator状态机自动关闭,无资源泄漏。
  • 错误隔离 :某一页请求失败,不影响其他页;可在Generator内部处理重试,外部消费逻辑完全无感。
  • UI响应 updateUI 可在每页处理后立即调用,提供流畅的进度反馈,避免“白屏卡死”。

这两个案例共同揭示了一个事实:Generator的价值,不在于它能让你写出多炫酷的代码,而在于它赋予了JavaScript一种 在资源受限环境下,依然能保持逻辑清晰、可维护、可扩展的工程能力 。它是JavaScript作为一门成熟系统编程语言的成人礼。

5. 避坑指南:那些年我们踩过的Generator深坑与填坑技巧

Generator强大,但初学者极易掉进几个隐蔽的坑。这些不是文档里会写的“注意事项”,而是我在生产环境里用服务器重启、用户投诉和深夜debug换来的血泪经验。

5.1 坑一: yield 只能在Generator函数内使用——但“内”字有陷阱

错误示例:

function* outer() {
  const inner = () => {
    yield 1; // ❌ SyntaxError: Unexpected reserved word 'yield'
  };
  inner();
}

你以为 inner outer 里面,就能用 yield ?错。 yield 是Generator函数的专属关键字,它要求 直接的词法作用域嵌套 inner 是一个普通箭头函数,它不认识 yield

正确解法:要么把 inner 也声明为Generator,要么用 yield* 委托:

function* outer() {
  // 方案1:inner也是Generator
  function* inner() {
    yield 1;
  }
  yield* inner(); // 使用yield*委托执行inner

  // 方案2:用普通函数返回值,由outer yield
  const getValue = () => 1;
  yield getValue();
}

提示: yield* 是Generator的“管道符”。它会将 inner() 返回的Iterator(Generator对象)的所有 yield 值,逐一 yield 出来,效果等同于把 inner 的代码体“展开”到 outer 里。这是实现Generator组合的基石。

5.2 坑二:Generator对象不是Iterator,而是Iterable

这是最常被混淆的概念。看这段代码:

function* gen() { yield 1; yield 2; }
const g = gen();

// ❌ 错误:Generator对象没有next()方法!
g.next(); // TypeError: g.next is not a function

// ✅ 正确:Generator对象是Iterable,需先获取Iterator
const iterator = g[Symbol.iterator]();
iterator.next(); // { value: 1, done: false }

但为什么 for...of 能直接用 g ?因为 for...of 语法糖会自动调用 g[Symbol.iterator]() 获取Iterator,再反复调用其 next() 。所以日常开发中,你几乎不会直接调用 g.next() ,而是用 for...of Array.from(g)

填坑技巧: 如果你需要手动控制,记住口诀:“Generator对象 → [Symbol.iterator]() → Iterator对象 → .next() ”。或者,更简单: 永远优先用 for...of ,除非你有特殊理由需要手动 next()

5.3 坑三: return 值的陷阱—— done: true 时的 value 归属

function* example() {
  yield 1;
  return 2; // 注意:这是return语句,不是yield
}

const g = example();
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: true } ← 关键!2是return值,不是yield值
console.log(g.next()); // { value: undefined, done: true } ← 后续永远返回undefined

这个陷阱在组合Generator时尤其危险。比如:

function* combine(a, b) {
  yield* a;
  yield* b;
  return 'done'; // 这个return会被谁接收?
}

const combined = combine(gen1(), gen2());
// 当combined执行完毕,它的return值'done'会成为combined.next()的最后一个value
// 但如果你用for...of,这个return值会被忽略!
for (const val of combined) {
  console.log(val); // 只打印gen1和gen2的yield值,'done'不会出现
}

填坑技巧: 如果你需要捕获组合Generator的 return 值,必须手动 next()

const iter = combined[Symbol.iterator]();
let result;
do {
  result = iter.next();
  if (!result.done) console.log(result.value);
} while (!result.done);
console.log('Final return:', result.value); // 'done'

5.4 坑四:异步Generator的 for await...of next() 的微妙差异

在Node.js或现代浏览器中, async function* 返回的是 AsyncGenerator ,其 next() 返回 Promise

async function* asyncGen() {
  yield await Promise.resolve(1);
  yield 2;
}

const ag = asyncGen();
// ❌ 错误:不能直接await ag.next()
// await ag.next(); // TypeError: Cannot read property 'then' of undefined

// ✅ 正确:await next()的返回值
const result1 = await ag.next(); // { value: 1, done: false }
const result2 = await ag.next(); // { value: 2, done: false }
const result3 = await ag.next(); // { value: undefined, done: true }

for await...of 会自动处理这个Promise:

for await (const val of asyncGen()) {
  console.log(val); // 1, 2
}

填坑技巧: 在异步场景下, 无脑用 for await...of 。只有当你需要精确控制 next() 的调用时机(如实现自定义的流控、超时中断),才手动 await ag.next() 。切记: await ag.next() 是正确的, await ag 是错误的( ag 本身不是Promise)。

这些坑,每一个都曾让我在凌晨两点对着控制台发呆。但填平它们的过程,恰恰是真正理解Generator运行时本质的过程。它不是魔法,而是一套严谨、可预测、有迹可循的机制。当你不再把它当作“高级语法”,而是当作一个“可调试的状态机”时,那些曾经的“坑”,就变成了你代码健壮性的护城河。

6. Generator与现代生态的共生:它不是孤岛,而是桥梁

Generator诞生于ES6,但它从未停留在“古老特性”的标签里。相反,它像一条沉默的地下河,持续滋养着JavaScript生态的演进。理解它与现代工具链的关系,能帮你做出更明智的技术选型。

6.1 Generator是 async/await 的基石:没有Generator,就没有优雅的异步

async/await 的语法糖,其底层实现完全依赖于Generator和Promise。Babel等转译器在编译 async 函数时,会将其重写为一个Generator函数,并用 co 或类似库自动驱动。看一个简化版的 async 转译逻辑:

// 你写的async函数
async function fetchData() {
  const res = await fetch('/api/data');
  const data = await res.json();
  return data;
}

// Babel可能转译为(概念上)
function fetchData() {
  return _regeneratorRuntime.mark(function _callee() {
    var res, data;
    return _regeneratorRuntime.wrap(function _callee$(_context) {
      while (1) {
        switch (_context.prev = _context.next) {
          case 0:
            _context.next = 2;
            return fetch('/api/data'); // yield一个Promise

          case 2:
            res = _context.sent; // Promise resolve后的值
            _context.next = 5;
            return res.json(); // yield另一个Promise

          case 5:
            data = _context.sent;
            return _context.abrupt("return", data);

          default:
            return _context.stop();
        }
      }
    }, _callee);
  });
}

_regeneratorRuntime 就是Babel注入的运行时,它本质上就是一个Generator驱动器:它调用 next() ,拿到 yield 出的Promise,用 .then() 注册回调,在回调里再次调用 next() ,如此循环,直到 done: true await 关键字,不过是 yield + 自动Promise处理的语法糖。

这意味着什么? 当你在项目中大量使用 async/await 时,你已经在无意识地使用Generator。理解Generator,就是理解 async/await 的“汇编语言”。当遇到难以调试的异步问题(如 await 不等待、 try/catch 失效),回溯到Generator层面,往往能找到根源。

6.2 Generator与RxJS:函数式响应式编程的底层共鸣

RxJS的 Observable ,其核心思想与Generator惊人地一致:都是“可订阅的数据源”。一个 Observable 可以被多次订阅,每次订阅都会创建一个独立的执行上下文(就像每次调用 gen() 都创建一个新的Generator实例)。 Observable.pipe() 操作符(如 map filter )与Generator的 yield* 组合,逻辑高度相似。

事实上,你可以用Generator轻松模拟一个简易 Observable

function fromGenerator(genFn) {
  return {
    subscribe: (observer) => {
      const gen = genFn();
      function next() {
        const { value, done } = gen.next();
        if (done) {
          observer.complete?.();
        } else {
          observer.next?.(value);
          setTimeout(next, 0); // 模拟异步调度
        }
      }
      next();
    }
  };
}

// 使用
fromGenerator(function* () {
  yield 1; yield 2; yield 3;
}).subscribe({
  next: x => console.log(x),
  complete: () => console.log('done')
});

这种思想同源性,解释了为什么学习Generator能极大降低RxJS的学习门槛。它们共享着“数据流”、“惰性求值”、“组合式编程”的DNA。

6.3 Generator与Web Workers:在主线程之外构建真正的流式处理

当你的Generator逻辑非常耗时(如复杂图像处理、密码学运算),阻塞主线程怎么办?答案是:把它扔进Web Worker。

// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'START_PROCESSING', data: hugeImageData });

worker.onmessage = (e) => {
  if (e.data.type === 'CHUNK') {
    renderChunk(e.data.chunk); // 主线程只做轻量渲染
  }
};

// worker.js
self.onmessage = (e) => {
  if (e.data.type === 'START_PROCESSING') {
    const generator = heavyImageProcessor(e.data.data);
    for (const chunk of generator) {
      self.postMessage({ type: 'CHUNK', chunk }); // 流式发送结果块
    }
  }
};

Generator在这里扮演了“跨线程数据流”的角色。Worker内部按需生成数据块,通过 postMessage 流式传递给主线程,避免了一次性传输巨大对象带来的序列化开销和内存峰值。这是Generator在高并发、高性能场景下的终极形态: 它让JavaScript拥有了在单线程模型下,实现类多线程流式协作的能力。

Generator不是被时代淘汰的遗迹,而是JavaScript进化树上一个承前启后的关键节点。它连接着ES6的初心,支撑着 async/await 的优雅,启发着RxJS的哲学,赋能着Web Workers的并行。掌握它,不是为了怀旧,而是为了看清脚下这片土地的地质构造,从而更自信地建造未来。

7. 我的实践心得:Generator不是“学了就用”,而是“用了才懂”

写了这么多年JavaScript,我越来越相信一个朴素的道理: 技术的深度,永远藏在它被反复使用的褶皱里,而不是初次接触的光滑表面。 Generator对我而言,正是这样一个需要“用时间去摩挲”的特性。

我第一次认真用Generator,是在一个物联网设备管理后台。客户要求实时展示10万台设备的在线状态热力图。后端API只提供分页查询( /devices?online=true&page=1&size=500 ),前端需要聚合所有在线设备的地理位置。最初的方案是“拉取全部数据再聚合”,结果页面加载15秒,内存飙升到2GB,Chrome直接崩溃。

绝望中,我翻出了尘封的ES6笔记,尝试用Generator重构:

async function* fetchAllOnlineDevices() {
  let page = 1;
  while (true) {
    const res = await fetch(`/devices?online=true&page=${page}&size=500`);
    const data = await res.json();
    yield* data.devices; // yield* 解构数组,逐个yield设备
    if (data.devices.length < 500) break;
    page++;
  }
}

// 在Canvas上绘制,每收到100个设备就刷新一次
let devices = [];
for await (const device of fetchAllOnlineDevices()) {
  devices.push(device);
  if (devices.length >= 100) {
    drawHeatmap(devices);
    devices = []; // 清空,释放内存
  }
}
drawHeatmap(devices); // 绘制剩余设备

那一刻的体验无法言喻:页面秒开,内存稳定在50MB,热力图随着数据流实时“生长”。这不是性能提升,而是 开发体验的质变 ——我不再需要和内存、超时、用户体验做艰苦卓绝的谈判,Generator替我完成了所有底层协调。

后来,我养成了一个习惯:每当看到代码里出现 for (let i = 0; i < arr.length; i++) ,尤其是 arr 来自网络或文件时,我会本能地问自己:“这个 arr ,能不能用Generator来‘流式’产生?”这个问题,已经帮我规避了至少七次潜在的OOM事故。

我也曾试图在所有地方滥用Generator,比如给一个只有3个元素的数组写 function* threeItems() { yield 1; yield 2; yield 3; } 。很快发现,这除了增加代码复杂度,毫无益处。 Generator的适用边界非常清晰:它专治“数据规模不确定”或“数据规模远超内存承载能力”的场景。 对于小数据, Array for 循环依然是最直接、最高效的。

最后分享一个小技巧:在VS Code里,给Generator函数加一个醒目的注释,提醒自己它的“流式”属性:

/**
 * @generator - Produces devices lazily. Do NOT convert to array unless necessary.
 * @yields {Device} Each online device object.
 */
async function* fetchAllOnlineDevices() { ... }

这行注释,是我和三个月后的自己之间的一份契约。它告诉我:这个函数不是普通的工具,而是一个需要被尊重的、有生命的数据流。理解Generator,最终不是为了记住 function* yield 的语法,而是为了培养一种 对数据规模、内存边界、执行效率的敬畏之心 。这种心智模式,才是它留给我最珍贵的遗产。

更多推荐