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

大家好!我是老码农,今天咱们聊聊 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 的几个特性:

  1. 暂停执行: 遇到 yield,函数就暂停执行,并且把 yield 后面的值返回给调用者。
  2. 恢复执行: 下次调用迭代器的 next() 方法,函数会从上次暂停的地方继续执行。
  3. 双向数据传递: 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);

在这个例子中:

  1. fetchData 是一个 Generator 函数。
  2. 我们用 yield 来等待异步操作完成。 yield getUser(1) 会暂停函数执行,直到 getUser(1) 返回 Promise 并 resolve,然后把 resolve 的值赋给 user 变量。
  3. run 函数是一个辅助函数,用来自动执行 Generator 函数。它会递归地调用 next() 方法,直到 donetrue

注意: run 函数的作用就是把 Generator 函数里的 yield 表达式,转换成 Promise,然后用 then() 方法来处理异步操作的结果。

对比一下:

特性 回调地狱 Generator 函数 + run 函数
代码结构 嵌套很深,难以阅读 线性结构,易于阅读
错误处理 容易出错,难以追踪 可以用 try...catch 统一处理
代码维护 困难 相对容易
异步操作处理 需要手动管理回调函数 通过 yield 自动管理,更简洁

四、Generator 函数的更多应用

除了异步编程,Generator 函数还有很多其他用途:

  1. 状态机: 可以用 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
  2. 惰性求值: 可以用 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
  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 函数很强大,但也有一些局限性:

  1. 需要辅助函数: 要处理异步操作,需要一个像 run 这样的辅助函数,或者使用 async/await
  2. 代码复杂度: Generator 函数的代码结构相对复杂,需要理解迭代器和 yield 的概念。
  3. 性能: 相对于直接使用 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 的其他有趣特性。

发表回复

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