一、迭代器:统一数据访问的"遥控器"
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 }
执行过程就像一个对话:
- 第一次
gen.next():函数开始执行,跑到第一个yield 1,暂停,把1还给调用方。 - 第二次
gen.next('A'):函数从上次暂停处恢复,'A'作为yield的返回值赋给a,继续跑到yield 2,把2还给调用方。 - 第三次
gen.next('B'):函数恢复,'B'赋给b,继续执行到return 3,结束。
生成器本质上是一个双向通道 :yield 向外"吐"值,next(arg) 向内"喂"值。这种暂停和恢复的能力,让它在处理异步流程时特别有用。
2.2 用生成器串联异步任务
假设页面初始化需要串行执行三个异步任务,每个都依赖前一个的结果:
- 获取用户信息
- 根据用户 ID 查权限
- 根据权限拉取菜单
传统写法容易陷入嵌套,用生成器可以让代码看起来像"同步"的:
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 的逻辑很简单:
- 调一次
gen.next(),拿到yield出的 Promise。 - 等 Promise 完成,把结果通过
gen.next(result)传回生成器内部。 - 重复以上两步,直到
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 的核心机制。 - 异步生成器 将"异步"和"按需消费"结合,适合处理逐步产出的异步数据。
我们不需要在业务代码里到处用生成器,但掌握以下三个"最小核心知识"还是很有必要的。
- 会用
for...of遍历数组,知道它背后靠的是Symbol.iterator。 - 能看懂生成器的
function*和yield语法,知道它能让函数暂停。 - 见过
for await...of的写法,知道它是用来处理异步流式数据的。
这些知识会帮助我们更好地理解 async/await 的底层原理,以及 Redux-Saga 等状态管理库的设计思路。当遇到需要"逐步消费一个异步数据源"的场景时,异步生成器几乎总是最佳答案。