生成器-高级迭代

常规函数只能返回一个值或一个对象,或者不返回值,但只要是返回,必然是一次返回全部数据。
然而生成器却允许按需一个一个的返回。

Generator 生成器

生成器函数

语法:

function* generateSequence() { // 方式一
  yield value;
}

写成function *generateSequence // 方式二 也是可以的

但规范偏向于方式一,更加侧重于这是一个特殊的函数,而非一个特殊的函数名

生成器函数被调用时并不会返回值,而是返回一个“generator object” 的特殊对象,来管理执行流程。

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

// "generator function" 创建了一个 "generator object"
let generator = generateSequence();
// 截止这一步,函数体其实并没有执行
console.info(generator); // [object Generator]

调用next方法才是真正的开始执行

let generator = generateSequence();

let one = generator.next();
// 拿到第一个值后,此时执行在 yield 2; 这一行暂停,等待下一次调用next方法
console.info(JSON.stringify(one)); // {value: 1, done: false}

let two = generator.next();

console.info(JSON.stringify(two)); // {value: 2, done: false}
// 直到第三次,所有值全部返回,显示结束
let three = generator.next();

console.info(JSON.stringify(three)); // {value: 3, done: true}

let four = generator.next();

console.info(JSON.stringify(four)); // {"done":true}

此后此生成器所有的next方法调用都将返回{"done":true}

可迭代

因此,允许使用for of进行循环遍历

使用循环获取值时,当done状态为true时,默认为结束,不显示

因此,不需要使用return返回,仅使用yield即可

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

let generator = generateSequence();

for(let value of generator) {
  console.info(value); // 1,然后是 2,然后是 3
}

// 意味着可以使用spread语法
let sequence = [0, ...generateSequence()];

console.info(sequence); // 0, 1, 2, 3

利用生成器进行迭代

最初的实现,利用自定义可迭代属性,将不可迭代对象进行可迭代处理

let range = {
  from: 1,
  to: 5,

  // for..of range 在一开始就调用一次这个方法
  [Symbol.iterator]() {
    // ...它返回 iterator object:
    // 后续的操作中,for..of 将只针对这个对象,并使用 next() 向它请求下一个值
    return {
      current: this.from,
      last: this.to,

      // for..of 循环在每次迭代时都会调用 next()
      next() {
        // 它应该以对象 {done:.., value :...} 的形式返回值
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// 迭代整个 range 对象,返回从 `range.from` 到 `range.to` 范围的所有数字
console.info([...range]); // 1,2,3,4,5

现在使用生成器函数

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

console.info( [...range] ); // 1,2,3,4,5

组合生成器

yield* 指令将执行 委托 给另一个 generator。

意味着 yield* gen 在 generator gen 上进行迭代,
并将其产出(yield)的值透明地(transparently)转发到外部。

generator 组合(composition)是将一个 generator 流插入到另一个 generator 流的自然的方式。
它不需要使用额外的内存来存储中间结果。

因此生成器对象在大数据对象返回迭代时具有天然优势,减小内存压力

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {

  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

console.info(str); // 0..9A..Za..z

双向通道

yield:不仅可以向外返回结果,而且还可以将外部的值传递到 generator 内。

  • 第一次调用 generator.next() 应该是不带参数的(如果带参数,那么该参数会被忽略)
  • yield 的结果进行返回 "2 + 2 = ?"
  • yield 接受参数4,赋值给ask1
  • yield 的结果进行返回 "3 * 3 = ?"
  • yield 接受参数9,赋值给ask2
  • 迭代结束,状态为true
function* gen() {
  let ask1 = yield "2 + 2 = ?";

  console.info(ask1); // 4

  let ask2 = yield "3 * 3 = ?"

  console.info(ask2); // 9
}

let generator = gen();

console.info( generator.next().value ); // "2 + 2 = ?"

console.info( generator.next(4).value ); // "3 * 3 = ?"

console.info( generator.next(9).done ); // true

// 甚至是异步执行
setTimeout(() => generator04.next(4), 1000);

generator.throw

对于正常值传入使用next方法即可,那对于错误的传入应当使用throw方法

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    console.info("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    console.info(e); // 显示这个 error
  }
}

let generator = gen(); // *1

let question = generator.next().value; // *2

generator.throw(new Error("The answer is not found in my database")); // (2)
  • *1行完成创建生成器对象
  • *2行进行生成器的第一次调用,此时返回 "2 + 2 = ?",等待传入值赋值给result
  • (2)行传入一个Error,相当于(1)行throw一个Error,直接被catch进行捕获
  • 进入catch模块,执行 console.info(e);
  • try模块代码被自动忽略

generator.return

完成 generator 的执行并返回给定的 value,此时生成器对象done状态为true,默认进入结束状态

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

const g = gen();

g.next();        // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next();        // { value: undefined, done: true }

异步迭代和 Generator

异步迭代

回顾可迭代对象

常规可迭代对象通过 Symbol.iterator 方法实现迭代能力:

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    return {
      current: this.from,
      last: this.to,

      next() {
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for(let value of range) {
  console.info(value); // 1, 2, 3, 4, 5
}

异步可迭代对象

当值需要异步获取时(如网络请求),使用 Symbol.asyncIterator

let range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() {
    return {
      current: this.from,
      last: this.to,

      async next() {
        await new Promise(resolve => setTimeout(resolve, 1000));

        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {
  for await (let value of range) {
    console.info(value); // 1, 2, 3, 4, 5(每秒一个)
  }
})();
特性迭代器 (Symbol.iterator)异步迭代器 (Symbol.asyncIterator)
方法名Symbol.iteratorSymbol.asyncIterator
next() 返回值任意值Promise
循环语法for..offor await..of

注意... 展开语法不能与异步迭代器一起工作,因为它期望同步的 Symbol.iterator

异步 Generator

回顾 Generator

常规 generator 使用 function*yield 生成值序列:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

for(let value of generateSequence(1, 5)) {
  console.info(value); // 1, 2, 3, 4, 5
}

Generator 可作为 Symbol.iterator 使代码更简洁:

let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() {
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

异步 Generator

function* 前添加 async,即可创建异步 generator:

async function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}

(async () => {
  for await (let value of generateSequence(1, 5)) {
    console.info(value); // 1, 2, 3, 4, 5(每秒一个)
  }
})();

异步 generator 可用作 Symbol.asyncIterator

let range = {
  from: 1,
  to: 5,

  async *[Symbol.asyncIterator]() {
    for(let value = this.from; value <= this.to; value++) {
      await new Promise(resolve => setTimeout(resolve, 1000));
      yield value;
    }
  }
};

(async () => {
  for await (let value of range) {
    console.info(value); // 1, 2, 3, 4, 5
  }
})();

技术上可同时实现 Symbol.iteratorSymbol.asyncIterator,但实际中很少这样做。

实际示例:分页数据获取

异步 generator 非常适合处理分页 API,如 GitHub commits:

async function* fetchCommits(repo) {
  let url = `https://api.github.com/repos/${repo}/commits`;

  while (url) {
    const response = await fetch(url, {
      headers: {'User-Agent': 'Our script'},
    });

    const body = await response.json();

    // 提取下一页 URL
    let nextPage = response.headers.get('Link')?.match(/<(.*?)>; rel="next"/);
    url = nextPage?.[1];

    for(let commit of body) {
      yield commit;
    }
  }
}

// 使用立即执行函数包裹调用
(async () => {
  let count = 0;

  for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
    console.log(commit.author.login);

    if (++count == 100) break;
  }
})();

关键点

  • 内部处理分页逻辑,对外暴露为简单的异步迭代
  • 使用 for await..of 即可遍历所有 commit,无需关心分页细节