阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 生成器与协程

生成器与协程

作者:陈川 阅读数:49097人阅读 分类: Node.js

生成器的基本概念

生成器是JavaScript中一种特殊的函数,它可以通过function*语法定义。与普通函数不同,生成器函数可以暂停执行并在稍后恢复,这种特性使得它非常适合处理异步操作和迭代场景。生成器函数执行时不会立即运行函数体,而是返回一个生成器对象。

function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = simpleGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3

生成器通过yield关键字暂停函数执行并返回一个值。每次调用生成器的next()方法时,函数会从上次暂停的位置继续执行,直到遇到下一个yield或函数结束。

生成器的内部机制

生成器函数在调用时不会立即执行函数体,而是返回一个实现了迭代器协议的生成器对象。这个对象维护着函数的执行上下文,包括局部变量、参数和当前的执行位置。当调用next()方法时,生成器会恢复执行直到遇到yield表达式。

function* counterGenerator() {
  let count = 0;
  while(true) {
    count += 1;
    yield count;
  }
}

const counter = counterGenerator();
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3

生成器还可以通过yield*表达式委托给另一个生成器,这在组合多个生成器时非常有用:

function* generatorA() {
  yield 'a';
  yield 'b';
}

function* generatorB() {
  yield* generatorA();
  yield 'c';
  yield 'd';
}

const gen = generatorB();
console.log([...gen]); // ['a', 'b', 'c', 'd']

协程的概念与实现

协程是一种比线程更轻量级的并发编程模型,它允许在单个线程内实现多个执行流的协作式调度。在JavaScript中,生成器可以看作是协程的一种实现方式,因为它们允许函数在执行过程中暂停和恢复。

协程与线程的主要区别在于:

  • 协程是协作式的,需要显式地让出控制权
  • 协程在用户空间实现,不依赖操作系统调度
  • 协程切换开销远小于线程切换
function* coroutineA() {
  console.log('A1');
  yield;
  console.log('A2');
  yield;
  console.log('A3');
}

function* coroutineB() {
  console.log('B1');
  yield;
  console.log('B2');
  yield;
  console.log('B3');
}

const a = coroutineA();
const b = coroutineB();

a.next(); // A1
b.next(); // B1
a.next(); // A2
b.next(); // B2
a.next(); // A3
b.next(); // B3

生成器与异步编程

生成器在Node.js中常用于简化异步代码的编写。结合Promise和生成器,可以实现类似同步代码风格的异步流程控制。这种模式在Koa框架中被广泛使用。

function asyncTask(value) {
  return new Promise(resolve => {
    setTimeout(() => resolve(value * 2), 1000);
  });
}

function* asyncGenerator() {
  const result1 = yield asyncTask(1);
  console.log(result1); // 2
  const result2 = yield asyncTask(2);
  console.log(result2); // 4
  return result1 + result2;
}

function runGenerator(gen) {
  const iterator = gen();
  
  function iterate(iteration) {
    if (iteration.done) return Promise.resolve(iteration.value);
    return Promise.resolve(iteration.value)
      .then(x => iterate(iterator.next(x)))
      .catch(e => iterate(iterator.throw(e)));
  }
  
  return iterate(iterator.next());
}

runGenerator(asyncGenerator).then(result => {
  console.log('Final result:', result); // 6
});

生成器与迭代器协议

生成器天然实现了迭代器协议,这使得它们可以无缝地与JavaScript的迭代语法配合使用。任何期望可迭代对象的地方都可以使用生成器。

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
  }
}

// 使用for...of循环
for (const n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

// 与数组解构配合
const [first, second, third] = fibonacci();
console.log(first, second, third); // 1 1 2

生成器的高级用法

生成器不仅可以产生值,还可以通过next()方法接收外部输入。这使得生成器可以实现双向通信,进一步增强了它们的灵活性。

function* twoWayGenerator() {
  const name = yield 'What is your name?';
  const age = yield `Hello ${name}, how old are you?`;
  return `${name} is ${age} years old`;
}

const gen = twoWayGenerator();
console.log(gen.next().value); // "What is your name?"
console.log(gen.next('Alice').value); // "Hello Alice, how old are you?"
console.log(gen.next(30).value); // "Alice is 30 years old"

生成器还可以用于实现状态机,将复杂的状态转换逻辑封装在生成器函数中:

function* trafficLight() {
  while (true) {
    yield 'red';
    yield 'yellow';
    yield 'green';
    yield 'yellow';
  }
}

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); // 'yellow'
console.log(light.next().value); // 'red'

Node.js中的实际应用

在Node.js生态系统中,生成器被广泛用于各种场景。Koa框架就是基于生成器构建的,它利用生成器来处理中间件流程。

const Koa = require('koa');
const app = new Koa();

app.use(function* (next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  this.set('X-Response-Time', `${ms}ms`);
});

app.use(function* (next) {
  const start = Date.now();
  yield next;
  const ms = Date.now() - start;
  console.log(`${this.method} ${this.url} - ${ms}`);
});

app.use(function* () {
  this.body = 'Hello World';
});

app.listen(3000);

生成器也常用于处理数据流,特别是在处理大型数据集时,可以有效地控制内存使用:

const fs = require('fs');
const readline = require('readline');

function* readLines(filePath) {
  const fileStream = fs.createReadStream(filePath);
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

async function processLargeFile() {
  const lineReader = readLines('large-file.txt');
  for (let i = 0; i < 10; i++) {
    const { value } = await lineReader.next();
    console.log(value);
  }
}

processLargeFile();

生成器与协程的性能考量

虽然生成器提供了强大的控制流抽象,但它们也有一些性能上的考虑。生成器的创建和切换比普通函数调用开销更大,因此在性能关键的代码路径中需要谨慎使用。

Node.js的V8引擎对生成器进行了优化,但在大规模使用时仍需注意:

  • 生成器对象的创建和销毁会有额外开销
  • 生成器切换比函数调用慢约2-5倍
  • 在热代码路径中频繁使用生成器可能影响性能
// 性能测试:生成器 vs 普通函数
function normalFunction(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += i;
  }
  return sum;
}

function* generatorFunction(n) {
  let sum = 0;
  for (let i = 0; i < n; i++) {
    sum += i;
    yield;
  }
  return sum;
}

console.time('normal');
normalFunction(1e6);
console.timeEnd('normal'); // 约1-3ms

console.time('generator');
const gen = generatorFunction(1e6);
let result;
while (!(result = gen.next()).done) {}
console.timeEnd('generator'); // 约10-30ms

生成器的错误处理

生成器提供了完善的错误处理机制,可以通过throw()方法将错误注入到生成器内部。这使得生成器可以像同步代码一样处理错误。

function* errorHandlingGenerator() {
  try {
    yield 'Step 1';
    yield 'Step 2';
    throw new Error('Something went wrong');
    yield 'Step 3';
  } catch (err) {
    console.log('Caught error:', err.message);
    yield 'Recovery step';
  } finally {
    console.log('Cleaning up');
    yield 'Cleanup step';
  }
}

const gen = errorHandlingGenerator();
console.log(gen.next().value); // "Step 1"
console.log(gen.next().value); // "Step 2"
console.log(gen.throw(new Error('Injected error')).value); // "Recovery step"
console.log(gen.next().value); // "Cleanup step"

生成器与递归

生成器可以很好地处理递归算法,特别是那些需要惰性求值或生成无限序列的场景。生成器递归通常使用yield*语法来实现。

function* traverseTree(node) {
  if (!node) return;
  yield node.value;
  yield* traverseTree(node.left);
  yield* traverseTree(node.right);
}

const tree = {
  value: 1,
  left: {
    value: 2,
    left: { value: 4 },
    right: { value: 5 }
  },
  right: {
    value: 3,
    left: { value: 6 },
    right: { value: 7 }
  }
};

console.log([...traverseTree(tree)]); // [1, 2, 4, 5, 3, 6, 7]

对于更复杂的递归场景,生成器可以避免栈溢出问题,因为它们可以逐步产生结果而不需要一次性计算所有值:

function* fibonacciSequence(n, prev = 0, curr = 1) {
  if (n <= 0) return;
  yield curr;
  yield* fibonacciSequence(n - 1, curr, prev + curr);
}

console.log([...fibonacciSequence(10)]); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

生成器与并发模式

生成器可以用于实现各种并发模式,如生产者-消费者模型、管道处理和并行任务协调。这些模式在Node.js的I/O密集型应用中特别有用。

function* producer(items) {
  for (const item of items) {
    console.log('Producing', item);
    yield item;
  }
}

function* consumer() {
  let count = 0;
  while (true) {
    const item = yield;
    console.log('Consuming', item);
    count++;
    if (count >= 5) break;
  }
}

function runPipeline() {
  const items = [1, 2, 3, 4, 5, 6, 7, 8];
  const prod = producer(items);
  const cons = consumer();
  
  cons.next(); // 启动消费者
  
  for (const item of prod) {
    cons.next(item); // 将每个项目传递给消费者
  }
}

runPipeline();

对于更复杂的并发控制,可以结合Promise和生成器实现协程池:

async function runInPool(generators, poolSize) {
  const executing = new Set();
  const results = [];
  
  for (const gen of generators) {
    const promise = Promise.resolve().then(() => gen.next().value);
    executing.add(promise);
    
    const cleanUp = () => executing.delete(promise);
    promise.then(cleanUp).catch(cleanUp);
    
    if (executing.size >= poolSize) {
      await Promise.race(executing);
    }
  }
  
  return Promise.all(executing);
}

const tasks = [
  function*() { yield asyncTask(1); return 1; },
  function*() { yield asyncTask(2); return 2; },
  function*() { yield asyncTask(3); return 3; },
  function*() { yield asyncTask(4); return 4; },
  function*() { yield asyncTask(5); return 5; }
];

runInPool(tasks, 2).then(console.log); // [1, 2, 3, 4, 5]

生成器的替代方案

虽然生成器功能强大,但在现代JavaScript中,async/await语法通常更简洁地解决了大多数异步编程问题。了解生成器与async/await的关系有助于更好地选择工具。

// 使用生成器
function* oldWay() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
}

// 使用async/await
async function newWay() {
  const a = await Promise.resolve(1);
  const b = await Promise.resolve(2);
  return a + b;
}

然而,生成器仍然在某些场景下具有优势:

  • 需要精细控制执行流程时
  • 实现自定义迭代逻辑时
  • 需要双向通信的协程时
  • 处理非Promise的异步值时
// 生成器处理非Promise异步值
function* eventGenerator() {
  const clicks = yield (next) => {
    document.addEventListener('click', next);
    return () => document.removeEventListener('click', next);
  };
  return `Clicked at ${clicks.clientX}, ${clicks.clientY}`;
}

function runEventGenerator(gen) {
  const iterator = gen();
  let cleanup;
  
  function step(value) {
    const result = iterator.next(value);
    if (result.done) return result.value;
    
    if (typeof result.value === 'function') {
      cleanup = result.value(step);
    } else {
      Promise.resolve(result.value).then(step);
    }
  }
  
  step();
  return () => cleanup && cleanup();
}

const stop = runEventGenerator(eventGenerator);
// 点击页面后会打印坐标
setTimeout(stop, 5000); // 5秒后停止监听

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌