迭代器、生成器与异步迭代——让数据“按需流动”的艺术

一、迭代器:统一数据访问的"遥控器"

1.1 for...of 为什么能遍历这么多类型?

我们肯定写过这样的代码:

javascript

ini 复制代码
const arr = ['a', 'b', 'c'];
for (const item of arr) {
  console.log(item);
}

for...of 不仅能遍历数组,还可以遍历 Map、Set、String、NodeList,甚至是 arguments 对象。到底是什么在背后支撑这种"统一"的能力?

答案就是迭代器模式 。你可以把迭代器想象成一个"遥控器":不管背后是电视、空调还是投影仪,遥控器上的"下一个频道"按钮的用法是一样的。JavaScript 中的迭代器就是一个对象,它提供了一个 next() 方法,每次调用都会返回下一个值。

那 for...of 怎么知道一个对象能不能被遍历呢?它看的是这个对象有没有实现 Symbol.iterator 方法。如果有,for...of 就调用它拿到"遥控器"(迭代器),然后反复按 next() 按钮获取下一个值,直到遥控器说"没了"(done: true)。

1.2 解剖一个迭代器:看看 next() 到底返回了什么

先来看看数组默认的迭代器是怎么工作的:

javascript

ruby 复制代码
const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator](); // 拿到遥控器

console.log(iterator.next()); // { value: 'a', done: false }  ← 第一个频道
console.log(iterator.next()); // { value: 'b', done: false }  ← 第二个频道
console.log(iterator.next()); // { value: 'c', done: false }  ← 第三个频道
console.log(iterator.next()); // { value: undefined, done: true } ← 没频道了

每次调用 next(),都会得到一个对象:{ value, done }。value 是当前的值,done 表示遍历是否结束。当你手动调用 next() 时,你完全控制了遍历节奏------这就是"按需消费"的核心。

二、生成器:一个可以"暂停"的函数

2.1 普通函数 vs 生成器函数

普通函数一旦执行,就像一列火车从起点开到终点,中途不停。但有时候我们希望函数能"暂停一下",比如:

  • 处理一批数据,每处理 100 条暂停一次,把主线程让给用户交互。
  • 串行调用多个接口,每个接口的结果决定下一个接口要不要发。

这时候就需要生成器函数 了。生成器函数在 function 后面加个 *,在函数体里用 yield 关键字设置"暂停点":

javascript

javascript 复制代码
function* simpleGenerator() {
  console.log('第一步:开始执行');
  const a = yield 1;        // 暂停点一:向外输出 1,等待外部传入值
  console.log('第二步:接收到外部传入的', a);
  const b = yield 2;        // 暂停点二:向外输出 2,等待外部传入值
  console.log('第三步:接收到外部传入的', b);
  return 3;                 // 结束:返回 3
}

const gen = simpleGenerator();

console.log(gen.next());      // 第一步:开始执行 → { value: 1, done: false }
console.log(gen.next('A'));   // 第二步:接收到外部传入的 A → { value: 2, done: false }
console.log(gen.next('B'));   // 第三步:接收到外部传入的 B → { value: 3, done: true }

执行过程就像一个对话:

  1. 第一次 gen.next() :函数开始执行,跑到第一个 yield 1,暂停,把 1 还给调用方。
  2. 第二次 gen.next('A') :函数从上次暂停处恢复,'A' 作为 yield 的返回值赋给 a,继续跑到 yield 2,把 2 还给调用方。
  3. 第三次 gen.next('B') :函数恢复,'B' 赋给 b,继续执行到 return 3,结束。

生成器本质上是一个双向通道 :yield 向外"吐"值,next(arg) 向内"喂"值。这种暂停和恢复的能力,让它在处理异步流程时特别有用。

2.2 用生成器串联异步任务

假设页面初始化需要串行执行三个异步任务,每个都依赖前一个的结果:

  1. 获取用户信息
  2. 根据用户 ID 查权限
  3. 根据权限拉取菜单

传统写法容易陷入嵌套,用生成器可以让代码看起来像"同步"的:

javascript

javascript 复制代码
function* initFlow() {
  const user = yield fetch('/api/user').then(r => r.json());
  console.log('用户信息', user);

  const permissions = yield fetch(`/api/permissions?userId=${user.id}`).then(r => r.json());
  console.log('权限', permissions);

  const menu = yield fetch(`/api/menu?role=${permissions.role}`).then(r => r.json());
  console.log('菜单', menu);

  return menu;
}

但这只是个"声明",还需要一个执行器来自动驱动它:

javascript

scss 复制代码
function runGenerator(genFunc) {
  const gen = genFunc();

  function handleNext(value) {
    const result = gen.next(value);
    if (result.done) {
      return Promise.resolve(result.value);
    }
    return Promise.resolve(result.value).then(handleNext);
  }

  return handleNext();
}

runGenerator(initFlow).then(menu => {
  console.log('初始化完成,最终菜单:', menu);
});

runGenerator 的逻辑很简单:

  1. 调一次 gen.next(),拿到 yield 出的 Promise。
  2. 等 Promise 完成,把结果通过 gen.next(result) 传回生成器内部。
  3. 重复以上两步,直到 done: true。

这个迷你执行器其实就是一个简化版的 async/await 引擎 !ES2017 的 async/await 正是生成器 + Promise 的语法糖(在底层用类似的机制实现了异步函数)。理解了这一点,你就知道 await 为什么能"暂停"异步函数而不阻塞主线程了------它本质上就是生成器的暂停恢复机制。


三、生成器在前端框架中的实战:Redux-Saga 与 dva

3.1 Redux-Saga:生成器管理副作用

Redux-Saga 是 Redux 生态中经典的副作用管理中间件。它的核心就是利用生成器函数:

javascript

php 复制代码
function* fetchUserSaga(action) {
  try {
    const user = yield call(api.fetchUser, action.payload.userId);
    yield put({ type: 'USER_FETCH_SUCCEEDED', user });
  } catch (e) {
    yield put({ type: 'USER_FETCH_FAILED', message: e.message });
  }
}

这里的 call 和 put 并不是真正的异步调用,它们只是普通的 JavaScript 对象(称为 Effect),描述了"我想要做什么"。Saga 中间件内部有一个类似我们上面写的 runGenerator 的执行器,每当生成器 yield 出一个 Effect 对象,就根据类型去执行真正的操作,然后把结果通过 gen.next(result) 传回去。

生成器给 Redux-Saga 带来的核心优势:

  • 副作用与组件分离:所有异步流程集中在 saga 文件里。
  • 可取消 :通过 takeLatest 等 Effect,自动取消上一次未完成的请求,解决"搜索竞态"问题。
  • 可测试 :单元测试时直接断言 gen.next().value 的结构,不需要 mock 网络请求。

3.2 dva:很多前端第一次真正写生成器,就是因为它

dva 是蚂蚁金服出品的轻量级前端框架,它的 Model 层直接内置了 Redux-Saga:

javascript

javascript 复制代码
export default {
  namespace: 'user',
  state: { list: [], loading: false },

  reducers: {
    save(state, { payload }) {
      return { ...state, list: payload };
    },
  },

  effects: {
    *fetchUsers({ payload }, { call, put }) {
      yield put({ type: 'setLoading', payload: true });
      try {
        const users = yield call(api.fetchUsers, payload);
        yield put({ type: 'save', payload: users });
      } catch (e) {
        console.error('获取用户列表失败', e);
      } finally {
        yield put({ type: 'setLoading', payload: false });
      }
    },
  },
};

几年前,dva曾经是很多团队的首选框架。大量前端开发者第一次在生产项目中真正接触生成器,就是在 dva 或 Redux-Saga 的场景下。 虽然现在 React Hooks 普及后,useEffect + async/await 覆盖了大部分组件级副作用,这两个库的使用量在下降,但如果你维护老项目或加入还在使用这些技术栈的团队,理解生成器就是理解项目核心数据流的关键。

四、异步迭代:让异步数据"边走边加载"

4.1 场景:逐页拉取接口数据

前面我们讲的 Symbol.iterator 和生成器都是同步的。但如果数据源是异步的------比如一个分页接口,每一页都需要等网络请求返回------普通的迭代器就不够用了。

假设我们需要逐页拉取几百页用户数据,传统写法需要手动管理页码:

javascript

ini 复制代码
let page = 1;
let hasMore = true;
while (hasMore) {
  const res = await fetch(`/api/users?page=${page}`);
  const data = await res.json();
  // 处理 data.items...
  hasMore = data.hasMore;
  page++;
}

这个逻辑散落在业务代码里,不易复用。

4.2 异步生成器:把"拉取"封装起来

ES2018 引入了异步生成器 (async function*),它既可以 yield 值,又可以用 await 等待异步操作。我们可以把分页拉取封装成一个异步生成器:

javascript

ini 复制代码
async function* fetchAllUsers() {
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const res = await fetch(`/api/users?page=${page}&size=20`);
    const data = await res.json();

    for (const user of data.items) {
      yield user; // 每次产出 1 个用户
    }

    hasMore = data.hasMore;
    page++;
  }
}

消费方用 for await...of 遍历,就像遍历一个普通数组一样:

javascript

arduino 复制代码
for await (const user of fetchAllUsers()) {
  console.log('加载用户:', user.name);
  // 可在此逐个渲染 DOM,或做其他处理
}

关键点 :for await...of 在每次 yield 后自动暂停,等下一轮循环需要下一个值时才继续。你不必手动管理页码,也不必一次性把所有数据加载到内存。这就是按需加载的优雅实现。

4.3 实战场景:大文件分片上传

大文件上传需要切片、逐片发送,还可以支持暂停和断点续传。用异步生成器管理这个流程非常自然:

javascript

javascript 复制代码
async function* sliceFile(file, chunkSize = 1024 * 1024) {
  let offset = 0;
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    offset += chunkSize;
    yield chunk; // 依次产出每个分片
  }
}

async function uploadFile(file) {
  const totalChunks = Math.ceil(file.size / (1024 * 1024));
  let uploaded = 0;

  for await (const chunk of sliceFile(file)) {
    const form = new FormData();
    form.append('chunk', chunk);
    form.append('index', uploaded);
    await fetch('/api/upload', { method: 'POST', body: form });
    uploaded++;
    console.log(`进度: ${uploaded}/${totalChunks}`);
  }

  console.log('上传完成');
}
  • 切片逻辑 (sliceFile)和上传逻辑 (uploadFile)完全分离,各司其职。
  • 如果用户点击"暂停",只需在 for await...of 循环内检查一个标志位,用 break 跳出,保存当前 uploaded 值即可。
  • 下次恢复时,从上次断点继续,不需要维护复杂的全局状态和回调链。

4.4 可取消的流式加载

配合 AbortController,异步生成器还能优雅地响应取消信号:

javascript

javascript 复制代码
async function* loadWithCancel(url, signal) {
  let page = 1;
  while (true) {
    if (signal.aborted) return;

    const res = await fetch(`${url}?page=${page}`, { signal });
    const data = await res.json();

    if (!data.items.length) break;

    for (const item of data.items) {
      yield item;
    }
    page++;
  }
}

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5 秒后取消

for await (const item of loadWithCancel('/api/data', controller.signal)) {
  console.log(item);
}

五、总结

迭代器和生成器,核心解决的都是数据流的控制问题:

  • 迭代器 统一了数据访问方式,让任何数据结构都能用 for...of 遍历。
  • 生成器 让函数可以暂停和恢复,是 async/await 的底层引擎,也是 Redux-Saga 和 dva 的核心机制。
  • 异步生成器 将"异步"和"按需消费"结合,适合处理逐步产出的异步数据。

我们不需要在业务代码里到处用生成器,但掌握以下三个"最小核心知识"还是很有必要的。

  1. 会用 for...of 遍历数组,知道它背后靠的是 Symbol.iterator。
  2. 能看懂生成器的 function* 和 yield 语法,知道它能让函数暂停。
  3. 见过 for await...of 的写法,知道它是用来处理异步流式数据的。

这些知识会帮助我们更好地理解 async/await 的底层原理,以及 Redux-Saga 等状态管理库的设计思路。当遇到需要"逐步消费一个异步数据源"的场景时,异步生成器几乎总是最佳答案。

相关推荐
定栓6 分钟前
Webpack迁到Vite第一节:模块系统迁移:require → import
运维·前端·webpack
小熊在忙fpga15 分钟前
N32G430芯片的flash表示
前端·javascript·html
Data analyse45621 分钟前
App埋点版本兼容的字段迁移校验
大数据·运维·服务器·前端·数据库·数据分析
NC 王炸27 分钟前
AI 做的便签,换个浏览器就没了?加一个导出按钮,把数据带走
javascript·ai编程·数据备份·localstorage·txt导出
todoitbo27 分钟前
本来只想给内网开个口子,Natapp 却多给了一道防护
前端·javascript·vue.js·内网穿透·网络防护·natapp
会灭火的程序员32 分钟前
SuperMap AgentX Server智能体接入WEB应用技术文档
前端·supermap·agentx·agentx server
CRMEB37 分钟前
商品 SKU 设计,从简单规格到多规格踩过什么
java·前端·数据库·开源
Chloe.Zz38 分钟前
一篇入门Agent
开发语言·前端·javascript
java1234_小锋1 小时前
【技术专题】HTML5 网页技术 - HTML5 表格标签
前端·html·html5
silvamux_1 小时前
4款新模型商务邮件翻译横评:0.98到0.46
android·java·javascript