阐述 JavaScript Generator (生成器) 函数的 yield 关键字如何实现暂停和恢复执行,并探讨其在异步编程中的应用。

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 或执行到函数末尾),donetrue;否则为 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 更是生成器的语法糖,让异步编程更加简洁易懂。
  • 生成器还可以应用于惰性求值、状态机、自定义迭代器等场景,为我们提供了更多的编程可能性。

掌握生成器,就像掌握了一把瑞士军刀,可以在各种场景下发挥它的作用。希望今天的讲座能帮助大家更好地理解和使用生成器,让你的代码更加优雅、高效。

感谢大家的聆听!希望咱们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注