你有没有想过,如果函数可以"暂停",等你想好了再继续,会是什么样?今天我们就来认识JavaScript里的"时间管理大师"------Generator函数。它能让你在执行到一半的时候停下来,等你喊"继续"再往下走。这听起来有点科幻,但它却是async/await的祖师爷。
前言
普通函数就像一支穿云箭,发射出去就直奔终点,中间绝不回头。但有时候我们需要更灵活的控制:比如我要分几步做一件事,每一步之间可能隔着十万八千里,或者我想让调用方决定什么时候继续。
Generator函数就是来解决这个问题的。它让你可以"暂停"函数执行,等会儿再"恢复"。这就像打游戏时按了暂停键,你去泡个面,回来继续打。
一、Generator长啥样?
Generator函数在function后面加个星号*,里面用yield关键字来"暂停"。
js
function* myGenerator() {
console.log('第一步');
yield '暂停一下';
console.log('第二步');
yield '再停一下';
console.log('第三步');
return '结束了';
}
调用这个函数并不会立即执行,而是返回一个迭代器对象 。你通过调用next()来一步步执行。
js
const gen = myGenerator();
console.log(gen.next()); // 输出:第一步,{ value: '暂停一下', done: false }
console.log(gen.next()); // 输出:第二步,{ value: '再停一下', done: false }
console.log(gen.next()); // 输出:第三步,{ value: '结束了', done: true }
console.log(gen.next()); // { value: undefined, done: true }
每次next()都会执行到下一个yield,然后暂停。yield后面的值会作为value返回。等所有代码执行完,done就变成true。
二、yield是"暂停键",next是"播放键"
这个机制有点像你写文章写到一半,突然想喝杯咖啡。你把光标停在某个位置(yield),喝完咖啡回来,再敲一下键盘(next),继续往下写。
更神奇的是,next()还可以传参 ,这个参数会成为上一个yield的返回值。这就像你暂停时给函数塞了张纸条,告诉它接下来该怎么走。
js
function* talkGenerator() {
const name = yield '你叫什么名字?';
const age = yield `${name},你多大了?`;
return `${name}今年${age}岁`;
}
const talk = talkGenerator();
console.log(talk.next()); // { value: '你叫什么名字?', done: false }
console.log(talk.next('张三')); // { value: '张三,你多大了?', done: false }
console.log(talk.next(18)); // { value: '张三今年18岁', done: true }
看到没?第一次next()只是启动,第二次next('张三')把"张三"传给了name,第三次传年龄。这就是Generator的"对话"能力。
三、协程:Generator的底层哲学
Generator函数的这种"暂停/恢复"能力,其实是**协程(Coroutine)**思想的体现。协程是一种比线程更轻量级的并发单元,它可以在多个任务之间主动让出控制权。
在JavaScript里,Generator就是协程的一种实现。你可以用它来模拟多任务协作,比如交替执行两个任务:
js
function* task1() {
yield '任务1: 第1步';
yield '任务1: 第2步';
return '任务1完成';
}
function* task2() {
yield '任务2: 第1步';
yield '任务2: 第2步';
return '任务2完成';
}
const t1 = task1();
const t2 = task2();
console.log(t1.next().value); // 任务1: 第1步
console.log(t2.next().value); // 任务2: 第1步
console.log(t1.next().value); // 任务1: 第2步
console.log(t2.next().value); // 任务2: 第2步
这样两个任务就像在"交替执行",但实际还是单线程,只是每次让出控制权。这就是"协作式多任务"。
四、Generator的"主战场":异步流程控制
在async/await出现之前,Generator是处理异步的利器。比如你要按顺序发起三个网络请求,用Promise可以这么写:
js
function fetchUser() { return fetch('/user').then(r => r.json()); }
function fetchOrders(userId) { return fetch(`/orders?userId=${userId}`).then(r => r.json()); }
function fetchProducts(orderId) { return fetch(`/products?orderId=${orderId}`).then(r => r.json()); }
// 用Generator + 自动执行器
function* fetchFlow() {
const user = yield fetchUser();
const orders = yield fetchOrders(user.id);
const products = yield fetchProducts(orders[0].id);
return products;
}
// 需要一个自动执行器,让yield后面的Promise自动执行
function run(generator) {
const gen = generator();
function step(result) {
if (result.done) return result.value;
return result.value.then(
res => step(gen.next(res)),
err => step(gen.throw(err))
);
}
return step(gen.next());
}
run(fetchFlow).then(products => console.log(products));
这个run函数就是传说中的自动执行器 ,它不断调用next,把Promise的结果传回去。这其实就是async/await的前身------用Generator模拟同步写法。
后来ES7直接把这种模式内置成了async/await,所以现在我们很少直接写Generator了,但它的思想深深影响了现代JS。
五、Generator的实用场景:不仅仅是异步
虽然有了async/await,Generator并没有被淘汰,它还在一些地方发光发热:
1. 无限数据结构
用Generator可以生成无限序列,比如斐波那契数列:
js
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
// 可以无限取下去
2. 状态机
Generator可以很方便地实现状态机,每个yield代表一个状态:
js
function* stateMachine() {
let state = 'idle';
while (true) {
const action = yield state;
switch (state) {
case 'idle':
if (action === 'start') state = 'running';
break;
case 'running':
if (action === 'pause') state = 'paused';
else if (action === 'stop') state = 'idle';
break;
case 'paused':
if (action === 'resume') state = 'running';
else if (action === 'stop') state = 'idle';
break;
}
}
}
const sm = stateMachine();
console.log(sm.next().value); // idle
console.log(sm.next('start').value); // running
console.log(sm.next('pause').value); // paused
console.log(sm.next('resume').value); // running
3. 简化迭代器
如果一个对象需要实现[Symbol.iterator],用Generator可以省掉很多模板代码:
js
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
for (const x of myIterable) {
console.log(x); // 1,2,3
}
六、Generator vs async/await
既然async/await已经这么方便,为什么还要学Generator?
- async/await:专注于异步,语法简洁,是处理异步任务的终极形态。
- Generator:更底层,更灵活,可以暂停任何操作(不仅仅是Promise),还可以用于创建迭代器、状态机等。
async/await本质上就是Generator + 自动执行器的语法糖。所以理解Generator,就能更深刻理解async/await的运作原理。
七、总结:Generator是JS里的"时间胶囊"
Generator函数让我们能够:
- 暂停函数执行,等以后再继续
- 通过
next传值,实现双向通信 - 用
yield实现惰性求值和无限序列 - 模拟协程,实现协作式多任务
- 为async/await打下基础
虽然现在很少直接写Generator做异步了,但它的思想无处不在。当你用for...of遍历数组时,背后有迭代器;当你用async/await时,底层有Generator的影子。
下次面试官问你"Generator有什么用",你可以告诉他:这是JavaScript的"时间管理大师",既能暂停时间,又能穿越时空,还能让异步代码看起来像同步。
明天我们将进入迭代器与可迭代对象 ,看看for...of、扩展运算符这些语法糖背后,到底藏着什么秘密。如果你觉得这篇文章够有趣,点个赞让更多人看到。我们明天见!