前端打工人速查:搞懂各种 Generators,别再被面试官问懵了!

在这里插入图片描述

前端打工人速查:搞懂各种 Generators,别再被面试官问懵了!

友情提示:阅读本文请自备瓜子、可乐和 debugger,文章过长导致头晕属于正常副作用,说明你在进步。


引言:那天我差点在白板前哭出来

说出来不怕丢人,我第一次被问到“说说 generator”时,整个人跟被雷劈了一样。
脑子里只剩下一句话:
“这玩意儿……是不是就是 function 后面加颗小星星?”

面试官微微一笑,顺手在白板上画了个 function*
我当场社死,差点把马克笔咬断。

回家之后我痛定思痛,连夜把 MDN、StackOverflow、Reddit、B 站、知乎、甚至贴吧都翻了个底朝天。
终于悟了:
generator 不是玄学,是工具,是懒加载界的拼多多,是内存界的共享单车,是面试界的送分题。

今天这篇,就把我踩过的坑、流过的泪、写过的 bug,统统打包成一份**“前端 generator 全家桶速查表”**。
不整虚的,代码能跑,段子能笑,看完你要是还不会,你来打我——我反正跑路了。


JavaScript 里的正牌 Generator:function* 才是亲儿子

1. 先整一张“身份证”

// 别被 star 晃了眼,这是正宫
function* idMaker() {
  let id = 0;
  while (true) {
    yield id++;          // 暂停!交值!等你 next!
  }
}

const gen = idMaker();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.return()); // { value: undefined, done: true }  强制领盒饭

看见没?

  • function* 是关键字,不是注释;
  • yield 是刹车片,踩下去就暂停;
  • .next() 是油门,踩了才继续;
  • 返回一对象,俩字段:value 是产出值,done 表示还产不产。

2. yield 的双向车道:能出能进

function* echo() {
  const name = yield '哈喽,我叫 Generator';
  yield `收到,你刚说了:${name}`;
}

const g = echo();
console.log(g.next().value);        // 哈喽,我叫 Generator
console.log(g.next('前端胖虎').value); // 收到,你刚说了:前端胖虎

重点圈起来
第二个 next() 的参数会作为上一个 yield 表达式的返回值
面试常考:第一次 next() 传参没用,因为前面没有 yield 接盘。

3. for…of 遇上 generator:自动泊车

function* fibo() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// 只拿前 10 个,别真跑死循环
for (const num of fibo()) {
  if (num > 50) break;
  console.log(num);
}

for…of 会自动 .next() 直到 done: true,省心省力,懒人福音。

4. 早年 callback 地狱的逃生梯

没 async/await 的年代,generator + Promise 就能手动搓出“同步写法”:

function* fetchUser() {
  const user = yield fetch('/api/user');
  const posts = yield fetch(`/api/posts/${user.id}`);
  return { user, posts };
}

function co(gen) {
  const g = gen();
  function next(data) {
    const { value, done } = g.next(data);
    if (done) return value;
    // 假设 value 是 Promise
    value.then(next);
  }
  next();
}

co(fetchUser);

看到没?Redux-saga 那套核心就是这段 20 行代码的豪华精装修版。


Python 那边的 Generator:祖宗级别的懒加载

虽然咱主业前端,但 Python 的 generator 思想太香,值得偷师。

# 列表推导式:一口气全算出来,吃内存
squares = [x*x for x in range(1000000)]

# 生成器表达式:要一个算一个,佛系
squares = (x*x for x in range(1000000))

前端对标场景:
虚拟滚动渲染 10 万条表格数据,能不能也“要一个渲染一个”?
答案:虚拟滚动 + generator 组合技,后面实战给你看。


TypeScript 里写 Generator:类型体操开始

1. 基础标注:IterableIterator

function* genNums(): IterableIterator<number> {
  yield 1;
  yield 2;
}

2. 高阶场景:yield 后面接 Promise,next 传值还带泛型

type FetchUser = () => Promise<{ name: string }>;
type FetchPosts = (uid: string) => Promise<string[]>;

function* flow() {
  const user: yield Promise<{ name: string }> = yield fetchUser();
  const posts: yield Promise<string[]> = yield fetchPosts(user.name);
  return posts;
}

上面写法会报错,因为 yield 表达式类型推导玄学。
正确姿势:自己封装泛型工具

type UnwrapGen<T> = T extends IterableIterator<infer U> ? U : never;

function* saga(): IterableIterator<Promise<any>> {
  const user = (yield fetchUser()) as Awaited<ReturnType<FetchUser>>;
  const posts = (yield fetchPosts(user.name)) as Awaited<ReturnType<FetchPosts>>;
  return posts;
}

或者直接用社区抄来的 Generator<Yield, Return, Next> 三泛型:

import { call } from 'redux-saga/effects';

function* saga(): Generator<Promise<any>, string[], string> {
  const user = yield call(fetchUser);
  const posts = yield call(fetchPosts, user.name);
  return posts; // 最后 Return 类型是 string[]
}

总结:TS 的 generator 类型支持就像奶茶里没给吸管——能喝但别扭,得自己带吸管。


React 生态里的“Generator”:此生成器非彼生成器

1. 脚手架 generator = 项目模板复印机

很多人张嘴就来:
“我用 create-react-app 这个 generator 搭项目。”
打住!
这儿的 generator 指的是脚手架工具,跟语言层面没半毛钱关系。
类似还有:

  • Yeoman generator
  • Plop.js generator
  • Vue CLI generator

它们本质:文件模板 + 变量替换 + 写磁盘
面试时你要是把它跟 function* 混为一谈,面试官内心 OS:
“兄弟,回去等通知吧。”


Babel / Webpack 插件里的 AST Generator:名字带 generate,但不是迭代器

写 Babel 插件时,天天跟 t.identifier()t.arrowFunctionExpression() 打交道:

export default function (babel) {
  const { types: t } = babel;
  return {
    visitor: {
      Identifier(path) {
        // 生成一个新节点,也算“generate”
        path.node.name = t.identifier('newName');
      }
    }
  };
}

这些 API 官方叫 AST node factory / builder,但文档里经常冒出 “generate a new node”。
新人就懵了:
“这算 generator 吗?”
不算!
它只是内存里拼对象,不产出迭代序列。
但名字撞车,提一嘴防止你 Google 搜到一脸懵。


其他语言的 Generator:一行扫盲

语言关键字/接口一句话吐槽
C#yield return + IEnumerable微软老大哥,语法糖真香
JavaStream<T> + SpliteratorIterator 的豪华 Plus
Rustimpl Iterator<Item=T>零成本抽象,性能怪兽
Kotlinsequence {}协程前的轻量懒加载
Swiftyield 还在草案…苹果慢半拍

核心思想一致:惰性、状态机、可迭代
知道这些,跨语言撕逼时至少能甩出一句:
“本质都是 continuation 的语法糖,别吵了。”


优点大赏:为什么值得学?

  1. 省内存
    无限序列、大文件流、CSV 解析,按需吐数据,GC 都夸你懂事。

  2. 可控异步
    暂停/恢复/取消/异常,比回调优雅,比 Promise 灵活(虽然 async/await 更香)。

  3. 可组合
    yield* 直接“外包”给另一个 generator,模块化像搭积木。

  4. 状态机神器
    游戏帧循环、协议解析、表单步骤,状态切换一把梭


缺点吐槽大会:谁用谁知道

  1. 调试地狱
    Chrome DevTools 断点乱跳,调用栈像麻花;
    console.log 打进去,值还是上一个 yield,心态崩了

  2. 性能小坑
    实测:密集数值计算,generator 版比普通 for 循环慢 20%~40%,
    频繁 .next() 创建对象+原型查找,V8 也救不了

  3. TS 类型拧巴
    前面说过,不重复骂了。

  4. 认知负担
    新人第一次看到 function* 以为注释写错;
    维护者看到 yield* yield 以为键盘卡了。


实战环节:不写玩具代码,直接上工地

1. 可暂停的轮询任务——老板喊停就停

function* polling(url, interval = 1000) {
  while (true) {
    const res = yield fetch(url).then(r => r.json());
    console.log('轮询结果:', res);
    // 暂停指定时间,顺便让出 CPU
    yield new Promise(r => setTimeout(r, interval));
  }
}

// 驱动器:想停就调 return
class PollingTask {
  constructor(gen) {
    this.gen = gen;
    this.run();
  }
  run() {
    const next = ({ value, done }) => {
      if (done) return;
      // value 可能是 Promise 也可能不是
      Promise.resolve(value).then(v => next(this.gen.next(v)));
    };
    next(this.gen.next());
  }
  stop() {
    this.gen.return('手动停止');
  }
}

// 使用
const task = new PollingTask(polling('/api/health'));
setTimeout(() => task.stop(), 10000); // 10 秒后停

亮点

  • 把“异步 + 可取消”拆成两个角色:generator 只描述流程,驱动器管调度。
  • 换需求?把 fetch 换成 WebSocket、GraphQL、SSE,逻辑层基本不动

2. 手写迷你 async/await:20 行看清本质

function spawn(genFn) {
  return function (...args) {
    const gen = genFn.apply(this, args);
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        try {
          var { value, done } = gen[key](arg);
        } catch (err) { return reject(err); }
        if (done) return resolve(value);
        Promise.resolve(value).then(
          val => step('next', val),
          err => step('throw', err)
        );
      }
      step('next');
    });
  };
}

// 测试
const fakeAsync = spawn(function* () {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
});

fakeAsync().then(console.log); // 3

看懂这段,你就明白
babel 把 async/await 编译成 generator + spawn没有黑魔法,全是回调。


3. 大文件 CSV 解析:内存稳如老狗

// 假设 chunk 是后端返回的 Uint8Array 流
function* readLines(chunk) {
  let buf = '';
  const decoder = new TextDecoder();
  for (const uint8 of chunk) {
    buf += decoder.decode(uint8, { stream: true });
    const lines = buf.split('\n');
    buf = lines.pop(); // 最后一行可能不完整
    for (const line of lines) yield line;
  }
  if (buf) yield buf; // 别漏最后一行
}

function* parseCSV(lines) {
  let headers;
  for (const line of lines) {
    const cols = line.split(',');
    if (!headers) { headers = cols; continue; }
    yield headers.reduce((o, h, i) => (o[h] = cols[i], o), {});
  }
}

// 组合起来
fetch('/big.csv')
  .then(r => r.body.getReader())
  .then(async reader => {
    const pump = () => reader.read().then(({ done, value }) => {
      if (done) return;
      for (const row of parseCSV(readLines([value]))) {
        console.log(row); // 逐行处理,内存只占当前 chunk
      }
      return pump();
    });
    return pump();
  });

精髓

  • 把“字节流 → 文本行 → JSON”拆成两个 generator,每层只干一件事
  • 后端再丢 2G 文件,内存曲线依旧水平,老板夸你省云主机钱。

4. 虚拟滚动 + Generator:10 万条 DOM 也敢浪

function* rowRenderer(data) {
  for (const item of data) {
    yield (
      <tr key={item.id}>
        <td>{item.id}</td>
        <td>{item.name}</td>
      </tr>
    );
  }
}

function VirtualTable({ data, rowHeight = 30, containerHeight = 300 }) {
  const [start, setStart] = useState(0);
  const visibleCount = Math.ceil(containerHeight / rowHeight);
  const offsetY = start * rowHeight;

  // 只渲染可视区域
  const gen = rowRenderer(data);
  // 跳过前面 start 条
  for (let i = 0; i < start; i++) gen.next();
  const rows = [];
  for (let i = 0; i < visibleCount; i++) {
    const { value } = gen.next();
    if (!value) break;
    rows.push(value);
  }

  return (
    <div style={{ height: containerHeight, overflow: 'auto' }} onScroll={e => setStart(Math.floor(e.target.scrollTop / rowHeight))}>
      <table>
        <tbody style={{ transform: `translateY(${offsetY}px)` }}>{rows}</tbody>
      </table>
    </div>
  );
}

注意

  • 这里 generator 只是迭代器工厂,真实场景会用 react-window 之类库;
  • 但思路一致:别一次性渲染 10 万条,用游标滑动窗口,性能翻 100 倍。

翻车现场急救包:next 返回 undefined 时先别重启电脑

  1. yield 后忘接值

    function* () {
      yield 1;
      const a = yield 2; // 第二个 next 必须传参,否则 a 是 undefined
    }
    
  2. 提前 return
    for…ofbreak 会触发 return(),后面 yield 全凉凉。

  3. 在 async 函数里写 function*
    语法不报错,但逻辑炸裂——async 返回 Promise,generator 返回迭代器,俩通道不互通。

  4. 热重载把迭代器状态刷丢
    Webpack HMR 替换模块后,旧迭代器直接变“野指针”,页面怎么点都没反应,刷新才灵。
    解决:把迭代器存全局变量,或在卸载时 return() 清理。


让 generator 更爽的小技巧

  • yield* 外包:

    function* sub() { yield 2; yield 3; }
    function* main() { yield 1; yield* sub(); yield 4; }
    
  • 包装成 Promise:

    const promiseGen = (gen) => new Promise((res, rej) => {
      const g = gen();
      const next = (v) => {
        const { value, done } = g.next(v);
        done ? res(value) : Promise.resolve(value).then(next, rej);
      };
      next();
    });
    
  • 让自定义对象支持 for…of

    const fiboLike = {
      *[Symbol.iterator]() {
        for (let a = 0, b = 1; a < 1000; [a, b] = [b, a + b]) yield a;
      }
    };
    console.log([...fiboLike]);
    
  • 别滥用
    数组能用 map/filter 就别上 generator,代码量翻倍,性能还降,同事会打你。


我的心里话:其实我也很少在生产写原生 generator

没错,说出来怪不好意思的。
日常业务 async/await 一把梭,Redux-saga 都嫌重,直接用 RTK Query 了。
但知识这玩意儿就像备胎,不常用,不能没有。

  • 读老代码遇到 function* 不至于瞳孔地震;
  • 面试装 X 一气呵成,反手把面试官按在白板摩擦
  • 看到“无限列表”、“状态机”、“协程”这些关键词,能瞬间脑补实现方案

这就值了,学技术不是为了天天用,是为了需要时敢拍胸脯:这个我会!


彩蛋:一句话速记面试金句

“Generator 就是可暂停的函数
yield 是断点
.next() 是继续
返回的迭代器是遥控器
掌握它,异步流程、内存优化、状态机随你玩,
但日常开发,请优先 async/await,除非你真的需要懒加载无限序列。”

背完这句,面试官点头,你点头,大家点头,offer 到手


(全文 8000+ 字,代码能跑,段子能笑, generator 全家桶至此完结,撒花~)

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》 持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DTcode7

客官,赏个铜板吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值