JavaScript 生成器:暂停、恢复与异步编程的妙用 (专家级讲座)
各位朋友,大家好!今天咱们来聊聊 JavaScript 里一个挺有意思的家伙——生成器 (Generator)。这玩意儿啊,就像个会魔法的函数,能让你的代码暂停,等你回过神儿了,再让它继续跑。听起来是不是有点像时间暂停器?
咱们今天就来揭开它的神秘面纱,看看 yield
关键字到底是怎么实现这种暂停和恢复的魔术,以及它在异步编程里能玩出什么花样。
一、生成器函数:不是函数,胜似函数
首先,咱们得认识一下生成器函数。别看它名字里带着“函数”俩字,但它跟普通的函数还真有点不一样。
1. 定义方式:
生成器函数用 function*
声明,注意那个 *
号,这可是它的身份标识。
function* myGenerator() {
console.log("生成器函数开始执行...");
yield 1; // 暂停,并返回 1
console.log("恢复执行...");
yield 2; // 再次暂停,并返回 2
console.log("生成器函数执行完毕!");
return 3;
}
2. 调用方式:
调用生成器函数不会立即执行里面的代码,而是返回一个 迭代器对象 (Iterator Object)。这个迭代器对象才是咱们操控生成器的关键。
const iterator = myGenerator();
console.log(iterator); // 输出: Object [Generator] {}
3. 迭代器对象:
这个迭代器对象有一个 next()
方法,每次调用 next()
,生成器函数就会从上次暂停的地方继续执行,直到遇到下一个 yield
或者 return
。
let result1 = iterator.next();
console.log(result1); // 输出: { value: 1, done: false }
let result2 = iterator.next();
console.log(result2); // 输出: { value: 2, done: false }
let result3 = iterator.next();
console.log(result3); // 输出: { value: 3, done: true }
咱们来总结一下:
特性 | 普通函数 | 生成器函数 |
---|---|---|
定义方式 | function myFunction() {} |
function* myGenerator() {} |
调用方式 | myFunction() |
myGenerator() (返回迭代器对象) |
执行方式 | 立即执行函数体 | 返回迭代器对象,需要调用 next() 逐步执行 |
返回值 | 返回一个值 (或 undefined ) |
迭代器对象,每次 next() 返回 {value, done} |
二、yield
关键字:暂停与恢复的魔法棒
yield
关键字是生成器函数的核心。它就像一个魔法棒,能让生成器函数暂停执行,并返回一个值。
1. 暂停执行:
当生成器函数执行到 yield
语句时,它会暂停执行,并将 yield
后面的表达式的值作为 value
属性返回给迭代器对象。
2. 返回值:
next()
方法返回一个对象,包含两个属性:
value
:yield
后面的表达式的值。done
: 一个布尔值,表示生成器函数是否执行完毕。如果执行完毕 (遇到return
或执行到函数末尾),done
为true
;否则为false
。
3. 恢复执行:
再次调用 next()
方法时,生成器函数会从上次 yield
语句的下一行开始继续执行。
咱们来看个例子:
function* countTo(max) {
let i = 1;
while (i <= max) {
yield i;
i++;
}
}
const counter = countTo(5);
console.log(counter.next()); // 输出: { value: 1, done: false }
console.log(counter.next()); // 输出: { value: 2, done: false }
console.log(counter.next()); // 输出: { value: 3, done: false }
console.log(counter.next()); // 输出: { value: 4, done: false }
console.log(counter.next()); // 输出: { value: 5, done: false }
console.log(counter.next()); // 输出: { value: undefined, done: true }
这个 countTo
生成器函数就像一个计数器,每次调用 next()
,它都会返回下一个数字,直到达到 max
值。
4. 传递值给 next()
:
next()
方法还可以接收一个参数,这个参数会作为上次 yield
表达式的返回值。
function* askName() {
const name = yield "What's your name?";
return `Hello, ${name}!`;
}
const asker = askName();
console.log(asker.next()); // 输出: { value: "What's your name?", done: false }
console.log(asker.next("Alice")); // 输出: { value: "Hello, Alice!", done: true }
在这个例子中,第一次调用 next()
,生成器函数暂停,并返回 "What’s your name?"。第二次调用 next("Alice")
,将 "Alice" 作为上次 yield
表达式的返回值,赋值给 name
变量,然后生成器函数继续执行,返回 "Hello, Alice!"。
三、生成器在异步编程中的应用:告别回调地狱
生成器在异步编程中最大的作用,就是让异步代码看起来更像同步代码,从而避免回调地狱。
1. 回调地狱的痛苦:
在没有 async/await
之前,处理异步操作通常使用回调函数。当多个异步操作依赖彼此的结果时,就会形成嵌套的回调函数,代码可读性极差,维护起来也很痛苦。这就是所谓的回调地狱。
// 回调地狱的例子
function getData(url, callback) {
setTimeout(() => {
const data = `Data from ${url}`;
callback(data);
}, 100);
}
getData("url1", (data1) => {
console.log(data1);
getData("url2", (data2) => {
console.log(data2);
getData("url3", (data3) => {
console.log(data3);
});
});
});
2. 生成器的救赎:
生成器可以配合 Promise 来处理异步操作,让代码看起来更清晰。
function getDataPromise(url) {
return new Promise((resolve) => {
setTimeout(() => {
const data = `Data from ${url}`;
resolve(data);
}, 100);
});
}
function* fetchData() {
const data1 = yield getDataPromise("url1");
console.log(data1);
const data2 = yield getDataPromise("url2");
console.log(data2);
const data3 = yield getDataPromise("url3");
console.log(data3);
}
function runGenerator(generator) {
const iterator = generator();
function handleNext(value) {
const next = iterator.next(value);
if (next.done) {
return;
}
// 如果 value 是 Promise,则等待 Promise 完成后再继续执行
if (next.value instanceof Promise) {
next.value.then(handleNext);
} else {
handleNext(next.value);
}
}
handleNext();
}
runGenerator(fetchData);
在这个例子中,fetchData
生成器函数使用 yield
关键字等待 Promise 完成,然后继续执行。runGenerator
函数负责驱动生成器函数执行,并处理 Promise 的解析。
3. async/await
的本质:
async/await
其实是生成器的语法糖。async
函数本质上就是一个返回 Promise 的生成器函数,而 await
关键字本质上就是 yield
关键字的简化版。
async function fetchDataAsync() {
const data1 = await getDataPromise("url1");
console.log(data1);
const data2 = await getDataPromise("url2");
console.log(data2);
const data3 = await getDataPromise("url3");
console.log(data3);
}
fetchDataAsync();
async/await
让异步代码看起来更像同步代码,极大地提高了代码的可读性和可维护性。
咱们来对比一下:
特性 | 回调函数 | 生成器 + Promise | async/await |
---|---|---|---|
代码结构 | 嵌套的回调函数 | 扁平化的代码结构 | 扁平化的代码结构 |
可读性 | 差 | 较好 | 很好 |
维护性 | 差 | 较好 | 很好 |
异步处理 | 通过回调函数处理异步结果 | 通过 yield 等待 Promise 完成 |
通过 await 等待 Promise 完成 |
本质 | 纯粹的回调 | 基于生成器和 Promise 的状态机 | 基于生成器和 Promise 的语法糖 |
四、生成器的应用场景:不止于异步
生成器除了在异步编程中大放异彩,还可以应用于其他场景。
1. 惰性求值 (Lazy Evaluation):
生成器可以用来实现惰性求值,即只在需要的时候才计算值。这在处理大数据集时非常有用。
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
这个 fibonacci
生成器函数可以无限生成斐波那契数列,但只有在调用 next()
方法时才会计算下一个值。
2. 状态机 (State Machine):
生成器可以用来实现状态机,控制程序在不同状态之间的转换。
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
这个 trafficLight
生成器函数模拟交通灯的状态转换。
3. 自定义迭代器:
生成器可以用来创建自定义迭代器,方便遍历各种数据结构。
const myObject = {
a: 1,
b: 2,
c: 3,
*[Symbol.iterator]() {
for (let key in this) {
if (this.hasOwnProperty(key)) {
yield this[key];
}
}
}
};
for (let value of myObject) {
console.log(value); // 输出: 1, 2, 3
}
在这个例子中,我们为 myObject
定义了一个生成器函数作为迭代器,可以方便地使用 for...of
循环遍历对象的属性值。
五、总结:生成器,妙不可言
生成器函数是 JavaScript 中一个强大的特性,它通过 yield
关键字实现了暂停和恢复执行的功能。
- 在异步编程中,生成器可以配合 Promise 来避免回调地狱,让异步代码看起来更像同步代码。
async/await
更是生成器的语法糖,让异步编程更加简洁易懂。 - 生成器还可以应用于惰性求值、状态机、自定义迭代器等场景,为我们提供了更多的编程可能性。
掌握生成器,就像掌握了一把瑞士军刀,可以在各种场景下发挥它的作用。希望今天的讲座能帮助大家更好地理解和使用生成器,让你的代码更加优雅、高效。
感谢大家的聆听!希望咱们下次再见!