JS `yield` 关键字:暂停与恢复 `Generator` 函数执行

各位观众,各位来宾,掌声欢迎!咳咳,大家好,我是今天的主讲人,江湖人称“代码老油条”。今天咱们聊聊 JavaScript 里一个挺有意思的家伙—— yield。别看它名字挺玄乎,其实用起来简单得很,学会了能让你的代码更优雅、更灵活,还能装个小小的逼,何乐而不为呢?

开场白:Generator 函数是个啥?

在深入 yield 之前,咱们先得搞清楚它的老巢—— Generator 函数。简单来说,Generator 函数就是个“能暂停和恢复执行”的函数。普通的函数,一旦开始执行,就得一口气跑到结束。但 Generator 函数不一样,它可以在执行过程中“喘口气”,把控制权交出去,等需要的时候再回来接着执行。

定义 Generator 函数,只需要在 function 关键字后面加个 * 就行了。

function* myGenerator() {
  console.log("Generator 函数开始执行");
  yield 1;
  console.log("第一次 yield 之后");
  yield 2;
  console.log("第二次 yield 之后");
  yield 3;
  console.log("Generator 函数执行完毕");
}

const generatorObject = myGenerator();

console.log(generatorObject.next()); // 输出: Generator 函数开始执行 { value: 1, done: false }
console.log(generatorObject.next()); // 输出: 第一次 yield 之后 { value: 2, done: false }
console.log(generatorObject.next()); // 输出: 第二次 yield 之后 { value: 3, done: false }
console.log(generatorObject.next()); // 输出: Generator 函数执行完毕 { value: undefined, done: true }

看到没?myGenerator() 并没有立即执行里面的代码,而是返回了一个 Generator 对象。我们需要通过调用 next() 方法来一步一步地执行它。

yield:暂停的魔法棒

现在主角登场了!yield 关键字的作用就是让 Generator 函数暂停执行,并且可以“吐”出一个值。这个值会作为 next() 方法返回对象的 value 属性。当 Generator 函数执行到 yield 语句时,它会:

  1. 暂停执行。
  2. yield 后面表达式的值作为 value 属性返回。
  3. 等待下一次 next() 调用。

直到我们再次调用 next() 方法,Generator 函数才会从上次 yield 的地方继续执行。当 Generator 函数执行完毕或者遇到 return 语句时,next() 方法返回对象的 done 属性会变成 true

next() 方法:唤醒沉睡的 Generator

next() 方法是控制 Generator 函数执行的遥控器。每次调用 next(),Generator 函数就会从上次暂停的地方恢复执行,直到遇到下一个 yield 或者执行完毕。

next() 方法还可以接受一个参数,这个参数会作为上一个 yield 表达式的返回值。这个特性非常有用,可以实现 Generator 函数和调用者之间的双向通信。

function* myGenerator() {
  const value = yield "第一次提问:你叫什么名字?";
  console.log("你回答说:", value);
  yield "第二次提问:你多大了?";
}

const generatorObject = myGenerator();

console.log(generatorObject.next()); // 输出: { value: '第一次提问:你叫什么名字?', done: false }
console.log(generatorObject.next("张三")); // 输出: 你回答说: 张三 { value: '第二次提问:你多大了?', done: false }
console.log(generatorObject.next()); // 输出: { value: undefined, done: true }

在这个例子中,next("张三") 将字符串 "张三" 传递给了上一个 yield 表达式,并赋值给变量 value

*`yield`:委派你的 Generator**

yield* 关键字可以将一个 Generator 函数的执行权委派给另一个 Generator 函数。这有点像函数调用,但区别在于 yield* 会把被委派的 Generator 函数的 yield 值依次返回,直到被委派的 Generator 函数执行完毕。

function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield* generator1();
  yield 3;
  yield 4;
}

const generatorObject = generator2();

console.log(generatorObject.next()); // 输出: { value: 1, done: false }
console.log(generatorObject.next()); // 输出: { value: 2, done: false }
console.log(generatorObject.next()); // 输出: { value: 3, done: false }
console.log(generatorObject.next()); // 输出: { value: 4, done: false }
console.log(generatorObject.next()); // 输出: { value: undefined, done: true }

yield* generator1() 会把 generator1()yield 值 1 和 2 依次返回,然后才会继续执行 generator2() 剩下的部分。

实际应用场景:Generator 的十八般武艺

Generator 函数和 yield 关键字的应用场景非常广泛,下面列举几个常用的:

  1. 异步编程:告别回调地狱

    Generator 函数可以和 Promise 结合,优雅地处理异步操作,避免回调地狱。

    function getData(url) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          const data = `从 ${url} 获取的数据`;
          resolve(data);
        }, 1000);
      });
    }
    
    function* fetchData() {
      try {
        const data1 = yield getData("url1");
        console.log("第一个数据:", data1);
        const data2 = yield getData("url2");
        console.log("第二个数据:", data2);
        const data3 = yield getData("url3");
        console.log("第三个数据:", data3);
      } catch (error) {
        console.error("发生错误:", error);
      }
    }
    
    function run(generator) {
      const iterator = generator();
    
      function handleNext(result) {
        if (result.done) {
          return;
        }
    
        const promise = result.value;
        promise.then(
          (data) => {
            handleNext(iterator.next(data));
          },
          (error) => {
            handleNext(iterator.throw(error));
          }
        );
      }
    
      handleNext(iterator.next());
    }
    
    run(fetchData);

    在这个例子中,fetchData() 函数使用 yield 暂停执行,等待 getData() 返回的 Promise 对象 resolve。run() 函数负责驱动 Generator 函数的执行,并处理 Promise 的 resolve 和 reject。

  2. 迭代器:自定义你的数据遍历方式

    Generator 函数可以轻松地创建迭代器,自定义数据的遍历方式。

    function* myIterator(array) {
      for (let i = 0; i < array.length; i++) {
        yield array[i];
      }
    }
    
    const myArray = [1, 2, 3, 4, 5];
    const iterator = myIterator(myArray);
    
    console.log(iterator.next()); // 输出: { value: 1, done: false }
    console.log(iterator.next()); // 输出: { value: 2, done: false }
    console.log(iterator.next()); // 输出: { value: 3, done: false }
    console.log(iterator.next()); // 输出: { value: 4, done: false }
    console.log(iterator.next()); // 输出: { value: 5, done: false }
    console.log(iterator.next()); // 输出: { value: undefined, done: true }
    
    // 使用 for...of 循环遍历
    for (const value of myIterator(myArray)) {
      console.log(value); // 输出: 1 2 3 4 5
    }

    myIterator() 函数创建了一个迭代器,可以依次返回数组中的每个元素。使用 for...of 循环可以方便地遍历这个迭代器。

  3. 状态机:简化你的状态管理

    Generator 函数可以用来实现状态机,简化状态管理。

    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() 函数模拟了一个交通灯的状态,使用 yield 依次返回不同的状态。

  4. 协同程序:实现并发效果

    Generator 函数可以用来实现协同程序,模拟并发效果。

    function* task1() {
      console.log("Task 1 开始");
      yield;
      console.log("Task 1 结束");
    }
    
    function* task2() {
      console.log("Task 2 开始");
      yield;
      console.log("Task 2 结束");
    }
    
    const t1 = task1();
    const t2 = task2();
    
    t1.next(); // Task 1 开始
    t2.next(); // Task 2 开始
    t1.next(); // Task 1 结束
    t2.next(); // Task 2 结束

    在这个例子中,task1()task2() 两个 Generator 函数交替执行,模拟了并发效果。

Generator 函数 vs. 普通函数:对比分析

为了更清晰地理解 Generator 函数的特点,我们将其与普通函数进行对比:

特性 普通函数 Generator 函数
定义方式 function myFunction() {} function* myGenerator() {}
执行方式 立即执行 返回 Generator 对象,需要调用 next() 方法执行
暂停与恢复 不支持 支持,使用 yield 关键字暂停,next() 方法恢复
返回值 使用 return 语句返回 使用 yield 关键字返回,next() 方法返回对象包含 valuedone 属性
适用场景 同步操作,简单的逻辑处理 异步操作,迭代器,状态机,协同程序等

使用 Generator 函数的注意事项

  • Generator 函数的执行需要依赖 next() 方法,需要仔细控制执行流程。
  • Generator 函数可以和 Promise 结合,但需要注意错误处理。
  • 过度使用 Generator 函数可能会导致代码可读性下降,需要权衡利弊。

总结:yield 的魅力

yield 关键字是 Generator 函数的核心,它赋予了函数暂停和恢复执行的能力,使得我们可以编写更灵活、更优雅的代码。虽然 Generator 函数的学习曲线稍微陡峭,但一旦掌握,你就能在异步编程、迭代器、状态机等领域大展身手。

希望今天的讲座能帮助大家更好地理解 yield 关键字和 Generator 函数。记住,编程的乐趣在于不断学习和探索,勇于尝试新的技术,才能成为真正的代码大师!

感谢大家的聆听,下课!

发表回复

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