Skip to content

Generator[生成器]

基本概念

  • Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

    执行 Generator 函数会返回一个遍历器对象,返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

  • 形式上,Generator 函数是一个普通函数,但是有两个特征:

    function关键字与函数名之间有一个* 星号;

    函数体内部使用yield表达式,定义不同的内部状态;

Generator 函数

javascript
function* name(param,...rest) {
  statements
}
function* name(param,...rest) {
  statements
}
  1. function* 声明创建一个 GeneratorFunction 对象(生成器函数)。
  2. 每次调用生成器函数时,它都会返回一个新的 Generator 对象(独立的),该对象符合迭代器协议
  3. 当迭代器的 next() 方法被调用时,生成器函数的主体会被执行,直到遇到第一个 yield 表达式
  4. function* 声明的行为与 function 声明类似,它们会被提升到其作用域的顶部,并且可以在当前作用域的任何位置被调用,且只能在特定的上下文中被重新声明。
  5. ES6 没有规定,关键字function*,要写在哪个位置。只要在函数名与关键中间。(推荐编码风格靠左)

①、yield 表达式指定了迭代器要返回的值,或者用 yield* 委托给另一个生成器函数。

②、next() 方法返回一个对象,其 value 属性包含了 yield 表达式的值,done 属性是布尔类型,表示生成器是否执行完毕。(false:未完成,true:完成)

③、如果 next() 方法带有参数,那么它会恢复生成器函数的执行,并用参数替换暂停执行的 yield 表达式。(此处要注意)

④、在生成器中执行 return 语句会使生成器结束(即返回的对象的 done 属性将被设置为 true)。如果返回一个值,它将被设置为生成器返回的对象的 value 属性。

⑤、如果生成器内部抛出错误,生成器也会结束,除非在生成器的代码体内捕获该错误。

⑥、生成器结束后,后续 next() 调用不会执行生成器的任何代码,只会返回一个形如 {value: undefined, done: true} 的对象。

javascript
// 1
function* idMaker(params) { 
  let index = 0;
  while (index < 4) {
    yield index++;
  }
}
// 2
const genFn = idMaker();
// 3
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next()); // { value: undefined, done: true }
// 1
function* idMaker(params) { 
  let index = 0;
  while (index < 4) {
    yield index++;
  }
}
// 2
const genFn = idMaker();
// 3
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next()); // { value: undefined, done: true }

yield

  1. yield 关键字用于 "暂停" 和 "恢复" 生成器函数
  2. yield 关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的 return 关键字。
  3. yield 关键字实际返回一个 IteratorResult 对象,它有两个属性,value 和 done。value 属性是对 yield 表达式求值的结果,而 done 是 false,表示生成器函数尚未完全完成。
  4. 一旦遇到 yield 表达式,生成器的代码将被暂停运行,直到生成器的 next() 方法被调用。每次调用生成器的 next() 方法时,生成器都会恢复执行,直到达到以下某个值:

    ①、yield,导致生成器再次暂停并返回生成器的新值。下一次调用 next() 时,在 yield 之后紧接着的语句继续执行。

    ②、throw 用于从生成器中抛出异常。这让生成器完全停止执行,并在调用者中继续执行,正如通常情况下抛出异常一样。

    ③、到达生成器函数的结尾。在这种情况下,生成器的执行结束,并且 IteratorResult 给调用者返回 value 的值是 undefined 并且 done 为 true。

    ④、到达 return 语句。在这种情况下,生成器的执行结束,并将 IteratorResult 返回给调用者,其 value 的值是由 return 语句指定的,并且 done 为 true。

  5. 如果将参数传递给生成器的 next() 方法,则该值将成为生成器当前 yield 操作返回的值。(next()将代替yield与后面的表达式)

yield*表达

yield* 表达式用于委托给另一个generator 或可迭代对象。

javascript
function* counter() {
  yield 1
  yield* [ 2, 3, 4]
  yield 5
}
const count = counter();
for(let i of count ){
  console.log(i)
}
// 1, 2, 3, 4, 5
function* counter() {
  yield 1
  yield* [ 2, 3, 4]
  yield 5
}
const count = counter();
for(let i of count ){
  console.log(i)
}
// 1, 2, 3, 4, 5

next() 方法的参数

javascript
next(value)
next(value)

yield表达式本身没有返回值(或者说总是返回undefined)。当next(value)带有参数时,value将作为yield 表达式的结果。

variable = yield expression;value 将被分给 variable。

注意

由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

javascript
function* genFn() {
    console.log(yield)
    console.log(yield)
    console.log(yield)
    console.log(yield)
}
const gen = genFn();
gen.next(10); // 不打印
gen.next(20); // 20
gen.next(30); // 30
gen.next(40); // 40
function* genFn() {
    console.log(yield)
    console.log(yield)
    console.log(yield)
    console.log(yield)
}
const gen = genFn();
gen.next(10); // 不打印
gen.next(20); // 20
gen.next(30); // 30
gen.next(40); // 40

Generator.prototype.return()

return() 方法在被调用时,可以看作是在生成器主体当前暂停的位置插入了一个 return value; 语句,跳出当前函数。

其中 value 是传入给 return() 方法的值。

通常调用 return(value) 将返回 {done: true, value: value }。

如果 yield 表达式被包含在 try...finally 块中,控制流不会退出函数体,而是进入 finally 块。在这种情况下,如果 finally 块中有更多 yield 表达式,返回的值可能会不同,done 甚至可能是 false。

return 与 try...finally

javascript
function* gen() {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield "cleanup";
  }
}
const g1 = gen();
g1.next(); // { value: 1, done: false }
// 在 try...finally 前暂停执行。
g1.return("early return"); // { value: 'early return', done: true }
const g2 = gen();
g2.next(); // { value: 1, done: false }
g2.next(); // { value: 2, done: false }
// 在 try...finally 中暂停执行。
g2.return("early return"); // { value: 'cleanup', done: false }
// 完成值被保留
g2.next(); // { value: 'early return', done: true }
// 生成器处于完成状态
g2.return("not so early return"); // { value: 'not so early return', done: true }
function* gen() {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield "cleanup";
  }
}
const g1 = gen();
g1.next(); // { value: 1, done: false }
// 在 try...finally 前暂停执行。
g1.return("early return"); // { value: 'early return', done: true }
const g2 = gen();
g2.next(); // { value: 1, done: false }
g2.next(); // { value: 2, done: false }
// 在 try...finally 中暂停执行。
g2.return("early return"); // { value: 'cleanup', done: false }
// 完成值被保留
g2.next(); // { value: 'early return', done: true }
// 生成器处于完成状态
g2.return("not so early return"); // { value: 'not so early return', done: true }

Generator.prototype.throw()

throw() 方法在被调用时,可以看作是在生成器主体当前暂停的位置插入了一个 throw exception; 语句。

其中 exception 是传入给 throw() 方法的异常。

通常调用 throw(exception) 将会导致生成器抛出异常。

如果 yield 表达式被包含在 try...finally 块中,错误可能会被捕获,并且控制流可以在错误处理后恢复,或者正常退出。

throw 与 try...finally

javascript
function* gen() {
    while (true) {
        try {
            yield 42;
            yield 43;
            yield 44;
            yield 45;
        } catch (e) {
            console.log(e);
        }
    }
}
const g = gen();
g.next(); // { value: 42, done: false }
g.throw(new Error("出现了些问题")); // Error: 出现了些问题
g.next()  // { value: 43, done: false }
g.next(); // { value: 44, done: false }
function* gen() {
    while (true) {
        try {
            yield 42;
            yield 43;
            yield 44;
            yield 45;
        } catch (e) {
            console.log(e);
        }
    }
}
const g = gen();
g.next(); // { value: 42, done: false }
g.throw(new Error("出现了些问题")); // Error: 出现了些问题
g.next()  // { value: 43, done: false }
g.next(); // { value: 44, done: false }

应用

部署 Iterator 接口
javascript
function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}
// foo 3
// bar 7
function* iterEntries(obj) {
  let keys = Object.keys(obj);
  for (let i=0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
  }
}

let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
  console.log(key, value);
}
// foo 3
// bar 7
异步操作的同步化表达
javascript
function* loadUI() {
  showLoadingScreen();              // 第1次的 next() 
  yield loadUIDataAsynchronously(); // 第1次的 next()
  hideLoadingScreen();              // 第2次的 next()
}
var loader = loadUI();
// 加载UI
loader.next() // 开启loading 加载UI
// 卸载UI
loader.next() // 关闭loading
function* loadUI() {
  showLoadingScreen();              // 第1次的 next() 
  yield loadUIDataAsynchronously(); // 第1次的 next()
  hideLoadingScreen();              // 第2次的 next()
}
var loader = loadUI();
// 加载UI
loader.next() // 开启loading 加载UI
// 卸载UI
loader.next() // 关闭loading
前端实现简单分页
javascript
function* getPage(list, pageSize = 1) {
  for (let index = 0; index < list.length; index += pageSize) {
    yield list.slice(index, index + pageSize);
  }
}

const list = [1, 2, 3, 4, 5, 6, 7, 8];
const page = getPage(list, 3); // Generator { }

page.next(); // { value: [1, 2, 3], done: false }
page.next(); // { value: [4, 5, 6], done: false }
page.next(); // { value: [7, 8], done: false }
page.next(); // { value: undefined, done: true }
function* getPage(list, pageSize = 1) {
  for (let index = 0; index < list.length; index += pageSize) {
    yield list.slice(index, index + pageSize);
  }
}

const list = [1, 2, 3, 4, 5, 6, 7, 8];
const page = getPage(list, 3); // Generator { }

page.next(); // { value: [1, 2, 3], done: false }
page.next(); // { value: [4, 5, 6], done: false }
page.next(); // { value: [7, 8], done: false }
page.next(); // { value: undefined, done: true }