大家好!我是老码农,今天咱们聊聊 JavaScript 里一个挺有意思的家伙——Generator 函数,重点说说它的 yield
关键字,看看它怎么让函数“暂停”和“恢复”,以及在异步编程里能玩出什么花样。
一、Generator 函数:不走寻常路的函数
先来个开胃小菜,看看什么是 Generator 函数。它和普通函数最大的区别就是,它不是一口气执行完的,而是可以分段执行。
function* myGenerator() {
console.log("第一段代码");
yield 1; // 暂停在这里,并且返回 1
console.log("第二段代码");
yield 2; // 暂停在这里,并且返回 2
console.log("第三段代码");
return 3; // 函数结束,返回 3
}
const gen = myGenerator(); // 注意:这里不会执行函数体!
console.log(gen.next()); // 输出:第一段代码 { value: 1, done: false }
console.log(gen.next()); // 输出:第二段代码 { value: 2, done: false }
console.log(gen.next()); // 输出:第三段代码 { value: 3, done: true }
console.log(gen.next()); // 输出:{ value: undefined, done: true }
看到了没?myGenerator()
函数前面有个 *
,这就是 Generator 函数的标志。 调用 Generator 函数不会立即执行函数体,而是返回一个迭代器对象 gen
。
每次调用 gen.next()
,函数才会执行到下一个 yield
语句,然后暂停,并返回一个对象,这个对象有两个属性:
value
:yield
后面表达式的值。如果没有yield
,或者yield
后面没有表达式,value
就是undefined
。done
: 表示函数是否执行完毕。false
表示还没完,true
表示结束了。
二、yield
关键字:暂停的艺术
yield
关键字是 Generator 函数的核心。它就像一个“暂停按钮”,让函数执行到这里就停下来,把控制权交出去。下次再被“唤醒”,就从 yield
语句之后开始继续执行。
你可以把 yield
想象成一个交通信号灯,绿灯亮的时候,函数可以继续跑;红灯亮的时候,函数就得乖乖停下来。
yield
的几个特性:
- 暂停执行: 遇到
yield
,函数就暂停执行,并且把yield
后面的值返回给调用者。 - 恢复执行: 下次调用迭代器的
next()
方法,函数会从上次暂停的地方继续执行。 -
双向数据传递:
yield
不仅能返回值,还能接收值!可以通过next()
方法传递参数给yield
表达式。function* myGenerator(initialValue) { console.log("初始值:", initialValue); const receivedValue = yield initialValue * 2; //暂停并返回,同时等待接收值 console.log("接收到的值:", receivedValue); return receivedValue + 10; } const gen = myGenerator(5); let result = gen.next(); // 输出: 初始值: 5 { value: 10, done: false } console.log(result); result = gen.next(20); // 输出: 接收到的值: 20 { value: 30, done: true } console.log(result);
在这个例子中,第一次调用
next()
方法时,yield initialValue * 2
执行,返回10
并暂停。第二次调用next(20)
方法时,20
作为yield
表达式的结果赋值给receivedValue
,然后函数继续执行,最终返回30
。
三、Generator 函数与异步编程:告别回调地狱
Generator 函数在异步编程里大显身手,主要就是为了解决“回调地狱”的问题。
什么是回调地狱?
如果你用过 Node.js,或者做过前端开发,肯定遇到过这种情况:一个异步操作完成之后,需要执行另一个异步操作,然后又需要执行另一个异步操作…… 结果代码就变成了这样:
asyncOperation1(function(result1) {
asyncOperation2(result1, function(result2) {
asyncOperation3(result2, function(result3) {
asyncOperation4(result3, function(result4) {
// ... 嵌套越来越深
});
});
});
});
这就是回调地狱!代码嵌套太深,可读性差,维护起来也很痛苦。
Generator 函数如何解决回调地狱?
Generator 函数可以把异步操作变成“同步”的写法,让代码看起来更清晰。
举个例子,假设我们有三个异步操作:getUser()
, getPosts()
, getComments()
。
function getUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ id: id, name: "张三" });
}, 500);
});
}
function getPosts(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([{ id: 1, title: "文章1", userId: userId }]);
}, 500);
});
}
function getComments(postId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([{ id: 1, content: "评论1", postId: postId }]);
}, 500);
});
}
现在,我们用 Generator 函数来处理这些异步操作:
function* fetchData() {
try {
const user = yield getUser(1);
console.log("用户信息:", user);
const posts = yield getPosts(user.id);
console.log("文章列表:", posts);
const comments = yield getComments(posts[0].id);
console.log("评论列表:", comments);
} catch (error) {
console.error("发生错误:", error);
}
}
// 自动执行 Generator 函数的辅助函数
function run(generator) {
const iterator = generator();
function handle(result) {
if (result.done) {
return result.value;
}
return Promise.resolve(result.value).then(res => {
return handle(iterator.next(res));
}, err => {
return handle(iterator.throw(err));
});
}
try {
return handle(iterator.next());
} catch (ex) {
return Promise.reject(ex);
}
}
run(fetchData);
在这个例子中:
fetchData
是一个 Generator 函数。- 我们用
yield
来等待异步操作完成。yield getUser(1)
会暂停函数执行,直到getUser(1)
返回 Promise 并 resolve,然后把 resolve 的值赋给user
变量。 run
函数是一个辅助函数,用来自动执行 Generator 函数。它会递归地调用next()
方法,直到done
为true
。
注意: run
函数的作用就是把 Generator 函数里的 yield
表达式,转换成 Promise,然后用 then()
方法来处理异步操作的结果。
对比一下:
特性 | 回调地狱 | Generator 函数 + run 函数 |
---|---|---|
代码结构 | 嵌套很深,难以阅读 | 线性结构,易于阅读 |
错误处理 | 容易出错,难以追踪 | 可以用 try...catch 统一处理 |
代码维护 | 困难 | 相对容易 |
异步操作处理 | 需要手动管理回调函数 | 通过 yield 自动管理,更简洁 |
四、Generator 函数的更多应用
除了异步编程,Generator 函数还有很多其他用途:
-
状态机: 可以用 Generator 函数来模拟状态机。每个
yield
语句代表一个状态。function* trafficLight() { while (true) { yield 'red'; yield 'yellow'; yield 'green'; } } const light = trafficLight(); console.log(light.next().value); // red console.log(light.next().value); // yellow console.log(light.next().value); // green console.log(light.next().value); // red
-
惰性求值: 可以用 Generator 函数来生成一个无限序列,只有在需要的时候才计算值。
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 console.log(fib.next().value); // 3
-
可迭代对象: Generator 函数本身就是一个可迭代对象,可以用
for...of
循环来遍历。function* range(start, end) { for (let i = start; i <= end; i++) { yield i; } } for (let num of range(1, 5)) { console.log(num); // 1 2 3 4 5 }
五、Generator 函数的局限性
虽然 Generator 函数很强大,但也有一些局限性:
- 需要辅助函数: 要处理异步操作,需要一个像
run
这样的辅助函数,或者使用async/await
。 - 代码复杂度: Generator 函数的代码结构相对复杂,需要理解迭代器和
yield
的概念。 - 性能: 相对于直接使用 Promise,Generator 函数的性能可能会稍差一些。
六、Generator 函数 vs. async/await
async/await
是 ES2017 引入的异步编程的语法糖,它比 Generator 函数更简洁、更易用。
async/await
本质上就是 Generator 函数的语法糖。async
函数内部会自动创建一个 Generator 函数,await
相当于 yield
。
对比一下:
特性 | Generator 函数 + run 函数 |
async/await |
---|---|---|
代码简洁性 | 相对复杂 | 更简洁 |
易用性 | 相对困难 | 更容易 |
错误处理 | 需要手动处理 | 自动处理 |
本质 | 基于迭代器 | 基于 Promise |
结论:
async/await
已经成为异步编程的首选方案。但是,理解 Generator 函数的原理,可以帮助你更好地理解 async/await
的工作方式。
七、总结
今天咱们聊了 JavaScript 的 Generator 函数,重点讲了 yield
关键字如何实现暂停和恢复执行,以及它在异步编程中的应用。
记住,Generator 函数就像一个“分段执行”的函数,yield
就像一个“暂停按钮”,让函数可以随时停下来,把控制权交出去。
虽然 async/await
已经成为主流,但是理解 Generator 函数的原理,可以帮助你更好地理解 JavaScript 的底层机制。
好了,今天的讲座就到这里。希望大家有所收获!下次有机会再跟大家聊聊 JavaScript 的其他有趣特性。