• JavaScript中Generator的用法
  • 发布于 1周前
  • 33 热度
    0 评论
  • 果酱
  • 20 粉丝 20 篇博客
  •   
今天我们来聊一个 JavaScript 不是特别常用的语法 Generator ,我在实际的项目开发中很少看到有人去用,可能因为它的语法相对复杂,使用 async/awiat 也都能实现,所以大家实际很少去使用。但是在一些复杂的流程控制、状态判断的需求场景中 Generator 还是挺好用的。今天我们就从基础到实践再来学习一下 Generator 。

Generator 简介
JavaScript Generator 是 E6 引入的一种新的函数类型,可以生成多个值序列,它们可以暂停执行和恢复执行,让我们可以更简单高效的实现迭代器。如果我们看到一个 function 后面加上一了个 * 号,那么它就是一个 Generator 函数:
function* myGenerator() {
  // Generator 函数体
}
Generator 函数可以使用 yield 语句来定义要生成的值序列。每当执行到 yield 语句时,Generator 函数会暂停执行并返回一个包含当前生成值的对象,随后执行流程被挂起,等待下一次调用生成器函数。它的返回值是一个迭代器,这个迭代器可以通过调用 next() 方法来获取下一个生成值。当 Generator 函数中所有的 yield 语句都已经执行完成后,返回的迭代器的 done 属性为 true,表示生成器函数已经结束(这里的流程描述比较抽象,我们后面用实际案例讲解会好一点)。

Generator 函数最早是由 Python 语言中的协程(Coroutine)概念演化而来,随后被引入到 E6 标准中,希望用它来提高 JavaScript 中处理异步编程的能力。

Generator 的基本语法
Generator 函数使用 function* 关键字定义,它可以包含多个 yield 表达式,用于控制函数执行的流程:
function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}
调用 Generator 函数并不会执行函数内部的代码,而是返回一个迭代器对象,通过调用这个对象的 next() 方法来执行函数的代码,并返回一个由 yield 表达式返回的值:
// 堆代码 duidaima.com
const myGeneratorIterator = myGenerator();
console.log(myGeneratorIterator.next()); // 输出 { value: 1, done: false }
console.log(myGeneratorIterator.next()); // 输出 { value: 2, done: false }
console.log(myGeneratorIterator.next()); // 输出 { value: 3, done: false }
console.log(myGeneratorIterator.next()); // 输出 { value: undefined, done: true }
Generator 函数在执行过程中,遇到 yield 表达式时会暂停函数的执行,并这个表达式的值返回给调用者。当再次调用 next() 方法时,函数会从暂停的地方继续执行;我们可以在函数中 return 一个最终的返回值,这个值会被包装在一个包含 value 和 done 属性的对象中返回:
function* myGenerator() {
  console.log('Start');
  yield 1;
  console.log('Middle');
  yield 2;
  console.log('End');
  return 'Done';
}

const myGeneratorIterator = myGenerator();
console.log(myGeneratorIterator.next()); // 输出 Start, { value: 1, done: false }
console.log(myGeneratorIterator.next()); // 输出 Middle, { value: 2, done: false }
console.log(myGeneratorIterator.next()); // 输出 End, { value: 'Done', done: true }

Generator 的高级用法
yield* 表达式
yield* 许我们在 Generator 函数中调用另一个 Generator 函数或可迭代对象。

当 Generator 函数执行到一个 yield* 表达式时,它会暂停执行,并且将执行权转移到另一个 Generator 函数或可迭代对象中。直到这个函数或对象迭代结束后,执行权才会返回到原 Generator 函数中。
function* foo() {
  yield 1;
  yield 2;
}

function* bar() {
  yield* foo();
  yield 3;
}

for (let value of bar()) {
  console.log(value); // 输出 1, 2, 3
}
在这个例子中,Generator 函数 bar() 中的 yield* foo() 表达式会调用 foo() 函数并将其迭代结果依次返回给 bar() 函数。

数据交互
在 Generator 函数中,可以使用 yield 表达式将数据返回给调用方,调用方可以通过 next() 方法向 Generator 函数传递数据。这样就可以在调用方和 Generator 函数之间实现数据交互。
function* foo() {
  let x = yield;
  yield x * 2;
}

let gen = foo();
gen.next(); // 启动生成器
gen.next(10); // 传递参数 10,输出 20
在这个例子中,foo() 函数会在第一次调用 next() 方法时停止在第一个 yield 语句处,等待外部传入的数据。然后,第二次调用 next() 方法时将外部传入的数据作为 yield 表达式的值,再向下执行,直到遇到下一个 yield 表达式返回数据。

实际使用案例
异步编程
Generator 函数也可以用来实现异步编程,它可以通过调用 next() 方法、yield 关键字配合 Promise 控制函数的执行状态:
function* myGenerator() {
  const result1 = yield new Promise((resolve) => setTimeout(() => resolve('first'), 1000));
  console.log(result1);
  const result2 = yield new Promise((resolve) => setTimeout(() => resolve('second'), 2000));
  console.log(result2);
  const result3 = yield new Promise((resolve) => setTimeout(() => resolve('third'), 3000));
  console.log(result3);
}

const generator = myGenerator();
const promise = generator.next().value;
promise.then((result) => generator.next(result).value)
  .then((result) => generator.next(result).value)
  .then((result) => generator.next(result).value);
看起来是不是和 async/await 的作用很像,下面是他们两种语法之间的一些对比:

优点:
1.控制流更加灵活:使用 Generator 函数可以控制异步操作的执行顺序,可以依次执行多个异步操作,并在每个操作执行完成后再执行下一个操作,控制流更加灵活。
2.可以重复利用 Generator 函数的状态:Generator 函数的状态可以保存在对象中,可以在需要的时候继续执行函数,并使用已经保存的状态继续执行异步操作。
3.更加通用:Generator 函数可以用于处理各种类型的异步操作,包括事件、回调、迭代器和 Promise 等等。
缺点:
1.代码复杂度较高:使用 Generator 函数可能会增加代码的复杂度,因为需要额外的代码和处理步骤。

2.可读性较差:相比于 async/await,Generator 函数的语法和代码结构相对较复杂,可读性不如 async/await。


控制异步流程
编写异步代码时使用 Generator 函数来控制流程也非常方便。假设有一个需求场景是从三个不同的 API 获取数据,并在所有数据都准备好后进行下一步处理。这时候就可以使用 Generator 函数来使这个异步控制流更清晰:
function* fetchAllData() {
  const data1 = yield fetch('api1');
  const data2 = yield fetch('api2');
  const data3 = yield fetch('api3');
  return [data1, data2, data3];
}

function run(generator) {
  const iterator = generator();

  function handle(iteratorResult) {
    if (iteratorResult.done) {
      return Promise.resolve(iteratorResult.value);
    }

    return Promise.resolve(iteratorResult.value)
      .then(res => handle(iterator.next(res)));
  }

  return handle(iterator.next());
}

run(fetchAllData).then(data => {
  // 处理所有数据
  console.log(data);
});
处理大数据节省内存
在处理大的数据集时,如果一次性将所有数据加载到内存中,会造成内存浪费和程序性能下降的问题。使用 Generator 函数可以实现将数据按需处理,逐个读取和转换数据,减少内存占用和提高程序性能。
function* dataGenerator() {
  let index = 0;
  while (true) {
    yield index++;
  }
}

function* processData(data, processFn) {
  for (let item of data) {
    yield processFn(item);
  }
}

const data = dataGenerator();

const processedData = processData(data, item => item * 2);

for (let i = 0; i < 500; i++) {
  console.log(processedData.next().value);
}
实现状态机
Generator 函数也可以用于实现状态机。状态机是一种数学模型,它由一组状态和状态之间的转换规则组成,可以用于描述系统的行为和状态。在实际开发中,状态机可以用于处理复杂的业务逻辑,例如表单验证、工作流程控制等。

使用 Generator 函数实现状态机的过程如下:
定义状态机的各种状态,每个状态对应一个 Generator 函数。
使用 Generator 函数的 yield 语句实现状态之间的转换。
在调用 Generator 函数时,使用一个循环来依次执行各个状态,直到状态机执行完成。
下面是一个使用 Generator 实现状态机的示例:
function* stateMachine() {
  let state = 'start';

  while (true) {
    switch (state) {
      case 'start':
        console.log('Enter start state');
        state = yield 'start';
        break;

      case 'middle':
        console.log('Enter middle state');
        state = yield 'middle';
        break;

      case 'end':
        console.log('Enter end state');
        state = yield 'end';
        break;
    }
  }
}

const sm = stateMachine();

console.log(sm.next().value); // Enter start state
console.log(sm.next('middle').value); // Enter middle state
console.log(sm.next('end').value); // Enter end state

最后
Generator 和 async/await 相比,语法更加复杂,需要手动控制执行流程,使用起来相对较为麻烦。这也是为什么我在开发中很少看到 Generator 使用的原因之一,这个语法确实没有 async/await 看起来那么简洁和容易理解。但是做一些复杂的控制流、状态机的处理时, Generator 还是非常好用的,可以让我们的流程更加清晰。
用户评论