美文网首页
Generator 函数

Generator 函数

作者: 了凡和纤风 | 来源:发表于2019-10-17 20:50 被阅读0次

    一、简介

    1.1、基本概念

    Generator 函数时 ES6 提供的一种异步编程解决方案。
    从语法上看,可以把它理解成一个状态机,封装了多个内部状态。
    执行 Generator 函数会返回一个遍历器对象,所以,也可以说它是一个遍历器对象生成函数

    形式上,Generator 函数有两个特征:

    • function 命令与函数名之间有一个星号
    • 函数体内部使用 yield 语句定义不同的内部状态
    function* helloWorldGenerator() {
      yield 'hello'
      yield 'world'
      return 'ending'
    }
    
    var hw = helloWorldGenerator()
    

    该函数有3个状态:hello、world、return 语句(结束执行)

    调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象——遍历器对象(Iterator Object)
    下一步,必须调用遍历器对象的next 方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,知道遇到下一条 yield 语句(或 return 语句)为止。即,Generator 函数时分段执行的,yield 语句是暂停执行的标记,而next 方法可以恢复执行

    hw.next()
    // {value: "hello", done: false}
    hw.next()
    // {value: "world", done: false}
    hw.next()
    // {value: "ending", done: true}
    hw.next()
    // {value: undefined, done: true}
    

    上面一个调用了 4 次 next 方法,value 属性就是当前 yield 语句的值,done 属性的值表示遍历是否结束

    第三次调用时,Generator 函数从上次 yield 语句停下的地方,一直执行到 return语句(如果没有 return 语句,就执行到函数结束)。next 方法返回的对象的 value 属性就是紧跟在 return 语句后面的表达式的值(如果没有 return 语句,则 value 属性的值为 undefined),done 属性的值 true 表示遍历已经结束

    第四次调用时,此时Generator 函数已经运行完毕,done 属性为 true,以后再调用 next 方法,返回的都是这个值

    调用 Generator 函数返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象 next 方法,就会返回一个有着 value 和 done 两个属性的对象。value 属性表示当前的内部状态的值,是 yield 语句后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。


    ES6 没有规定 function 关键字与函数名之间的星号在哪个位置,因此下面的写法都能通过

    function * fn() {}
    function *fn() {}
    function* fn() {} // 常用写法
    function*fn() {}
    

    1.2、yield 表达式

    yield 语句就是暂停标志

    遍历器对象的 next 方法的运行逻辑如下:

    1. 遇到 yield 语句就暂停执行后面的操作,并紧跟在 yield 后面的表达式的值作为返回的对象的value 属性值。
    2. 下一次调用 next 方法是再继续往下执行,直到遇到下一条 yield 语句。
    3. 如果没有遇到新的 yield 语句,就一直运行到函数结束,知道 return 语句为止,并将 return 语句后面的表达式的值作为返回对象的 value 属性值。
    4. 如果该函数没有 return 语句,则返回对象的 value 属性值为 undefined

    注意:只要调用 next 方法且内部指针指向该语句时才会执行 yield 语句后面的表达式,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

    function* gen() {
      yield 123 + 456
    }
    

    上面的代码中,yield 后面的表达式 123 + 456 不会立即求值,只会在 next 方法将指针移到这一句时才求值


    yield 和 return的异同:
    相同之处

    • 都能返回紧跟在语句后面的表达式的值

    不同之处

    • 每次遇到 yield 函数暂停执行,下一次会从该位置继续向后执行,而 return 语句不具备位置记忆的功能
    • 一个函数里面只能执行 一次(或者说一条)return语句,但是可以执行多次(或者说多条)yield 语句。
    • 正常函数只能返回一个值,因为只能执行一次 return 语句;Generator 函数可以返回一系列的值,因为可以有任意多条 yield 语句。

    Generator 函数可以不同 yield 语句,这时就变成了一个单纯的暂缓执行函数

    function* fn() {
      console.log('执行了!')
    }
    
    var generator = fn()
    
    setTimeout(() => {
      generator.next()
    }, 2000)
    

    注意:yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错
    下面是一个例子

    var arr = [1, [[2, 3], 4], [5, 6]]
    
    var flat = function* (a) {
      a.forEach(function(item) {
        if (typeof item !== 'number') {
          yield* flat(item)
        } else {
          yield item
        }
      })
    }
    
    for (var f of flat(arr)) {
      console.log(f)
    }
    

    上面的代码,会产生句法错误,因为 forEach 方法的参数是一个普通函数,但是在里面使用了 yield 表达式(这个函数里面还使用了 yield* 表达式)

    另外,yield 表达式如果用在另一个表达式之中,必须放在圆括号里面

    function* demo() {
      console.log('Hello' + yield) // SyntaxError
      console.log('Hello' + yield 123) // SyntaxError
    
      console.log('Hello' + (yield)) // OK
      console.log('Hello' + (yield 123)) // oK
    }
    

    yield 表达式用作函数或放在赋值表达式的右边,可以不加括号

    function* demo() {
      foo(yield 'a', yield 'b') // OK
      let input = yield // OK
    }
    

    1.3、与 Iterator 接口的关系

    任意一个对象的 Symbol.iterator 方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象由于 Generator 函数就是遍历器生成函数,因此可以把Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口

    var myIterable = {}
    myIterable[Symbol.iterator] = function* () {
      yield 1
      yield 2
      yield 3
    };
    
    [...myIterable] // [1, 2, 3]
    

    Generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身

    function* gen() {
      // ...
    }
    
    var g = gen()
    
    g[Symbol.iterator]() === g
    // true
    

    g 是一个 Generator 函数,调用它会生成一个遍历器对象 g。它的 Symbol.iterator 属性也是一个遍历器对象生成函数,执行后返回它自己

    二、next 方法的参数

    yield 语句本身没有返回值,或者说总是返回 undefined。next 方法可以带有一个参数,该参数会被当作上一条 yield 语句的返回值。

    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1 }
      }
    }
    
    var g = f()
    g.next()
    // {value: 0, done: false}
    g.next()
    // {value: 1, done: false}
    g.next(true)
    // {value: 0, done: false}
    

    上面的代码先定义了一个可以无限运行的 Generator 函数f,如果next 方法设有参数,每次运行到 yield 语句时,变量 reset 的值总是 undefined。当 next 方法带有一个 参数 true时,当前遍历 reset 就被重置为这个参数(即 true),因而 i 会等于 -1,下一轮循环就从 -1开始递增。

    这个功能有很重要的语法意义。通过 next 方法的参数就有办法在 Generator 函数开始运行后继续向函数体内注入值。即,可以在 Generator 函数运行的不同阶段从外部向内部注入不同的值,从而调整函数行为。

    function* foo(x) {
      var y = 2 * (yield (x + 1))
      var z = yield (y / 3)
      return (x + y + z)
    }
    
    var a = foo(5)
    a.next()
    // {value: 6, done: false}
    a.next()
    // {value: NaN, done: false}
    a.next()
    // {value: NaN, done: true}
    
    var b = foo(5)
    b.next()
    // {value: 6, done: false}
    b.next(12)
    // {valu0e: 8, done: false}
    b.next(13)
    // {value: 42, done: true}
    

    注意:由于 next 方法的参数表示上一条 yield 语句的返回值,所以第一次使用 next 方法时传递参数是无效的。

    如果想要在第一次调用 next 方法时就能够传入值,可以在 Generator 函数外面再包一层。

    function wrapper(generatorFunction) {
      return function (...args) {
        let generatorObject = generatorFunction(...args)
        generatorObject.next()
        return generatorObject
      }
    }
    
    const wrapped = wrapper(function* () {
      console.log(`First input:${yield}`)
      return 'DONE'
    })
    
    wrapped().next('hello')
    // First input: hello
    

    上面的代码中,Generator 函数如果不同 Wrapper 先包一层,是无法在 第一次调用 next 方法时就输入参数的。

    function* dataConsumer() {
      console.log('Started')
      console.log(`1. ${yield}`)
      console.log(`2. ${yield}`)
      return 'result'
    }
    
    let genObj = dataConsumer()
    genObj.next()
    // Started
    genObj.next('a')
    //1. a
    genObj.next('b')
    // 2. b
    

    上面的代码是一个很直观的例子,每次通过 next 方法向 Generator 函数输入值,然后打印出来。

    for...of 循环

    for...of 循环可以自动遍历 Generator 函数生成的 Iterator 对象,且此时不再需要调用 next 方法

    function* foo() {
      yield 1
      yield 2
      yield 3
      yield 4
      return 5
    }
    
    for (let v of foo()) {
      console.log(v)
    }
    // 1 2 3 4
    

    一旦 next 方法的返回对象的 done 属性为 true,for...of 循环就会终止,且不包含该返回对象,所以上面的 return 语句返回的值不包括在 for...of 循环中

    利用 Generator 函数 和 for...of 循环实现斐波那契数列

    function* fibonacci() {
      let [prev, curr] = [0, 1]
      for(;;) {
        [prev, curr] = [curr, prev + curr];
        yield curr
      }
    }
    
    for (let n of fibonacci()) {
      if (n > 1000) break; // 1000 以内
      console.log(n)
    }
    

    利用 for...of 循环,可以写出遍历任意对象的方法。原生的JavaScript对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口后就可用了。

    function* objectEntries(obj) {
      let propKeys = Reflect.ownKeys(obj)
    
      for (let propKey of propKeys) {
        yield [propKey, obj[propKey]]
      }
    }
    
    let jane = { first: 'Jane', last: 'Doe' }
    
    for (let [key, value] of objectEntries(jane)){
      console.log(`${key}:${value}`)
    }
    // first:Jane
    // last:Doe
    

    另一种写法,将 Generator 函数加到对象的 Symbol.iterator 属性上。

    function* objectEntries() {
      let propKeys = Object.keys(this)
    
      for (let propKey of propKeys ) {
        yield [propKey, this[propKey]]
      }
    }
    
    let jane = { first: 'Jane', last: 'Done' }
    
    jane[Symbol.iterator] = objectEntries
    
    for (let [key, value] of jane ) {
      console.log(`${key}:${value}`)
    }
    // first:Jane
    // last:Doe
    

    除了 for...of 循环,扩展运算符(...)、结构赋值和 Array.from 方法内部调用的 都是遍历器接口。他们都可以将 Generator 函数返回的 Iterator 对象作为参数。

    function* numbers() {
      yield 1
      yield 2
      return 3
      yeild 4
    }
    
    // 扩展运算符
    [...numbers()] // 1 2
    
    // Array.from 方法
    Array.from(numbers()) // [1, 2]
    
    // 结构赋值
    let [x, y] = numbers()
    x // 1
    y // 2
    
    // for...of 循环
    for (let n of numbers()) {
      console.log(n)
    }
    // 1
    // 2
    

    四、Generator.prototype.throw()

    Generator 函数返回的遍历器对象都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

    var g = function* () {
      try {
        yield 
      } catch(e) {
        console.log('内部捕捉', e)
      }
    }
    
    var i = g ()
    i.next()
    
    try {
      i.throw('a')
      i.throw('b')
    } catch(e) {
      console.log('外部捕获', e)
    }
    // 内部捕捉 a
    // 外部捕获 b
    

    上面的代码,i 连续抛出了两个错误。第一个错误被 Generator 函数体内的 catch 语句捕获。i 第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被跑出来 Generator 函数体,被外部的 catch 语句捕获

    throw 方法可以接受一个参数,该参数会被 catch 语句接收建议抛出 Error 对象的实例

    var g = function* () {
      try {
        yield
      } catch(e) {
        console.log(e)
      }
    }
    
    var i = g()
    i.next()
    i.throw(new Error('出错了!'))
    // Error: 出错了
    

    需要注意的是,遍历器对象的 throw,和全局的 throw 命令。前面的例子中使用的是遍历器对象的 throw 方法,而不是throw 命令抛出的。后者只能被函数体外的 catch 语句捕获

    如果 Generator 函数内部没有部署 try...catch 代码块,那么 throw 方法抛出的错误将被外部 try...catch 代码块捕获。

    var g = function* () {
      while (true) {
        yield;
        console.log('内部捕获', e)
      }
    }
    
    var i = g()
    i.next()
    
    try {
      i.throw('a')
      i.throw('b')
    } catch(e) {
      console.log('外部捕获', e)
    }
    // 外部捕获 a
    

    上面的代码中,遍历器函数 g 内部没有部署 try...catch 代码块,所以抛出的错误直接被外部 catch 代码块捕获。

    如果 Generator 函数内部部署了 try...catch 代码块,那么遍历器的 throw 方法抛出的 错误不影响下一次遍历,否则遍历直接终止

    var gen = function* gen() {
      yield console.log('hello')
      yield console.log('world')
    }
    
    var g = gen()
    g.next()
    g.throw()
    // hello
    // Uncaught undefined
    

    上面没有部署 try...catch 代码块,导致程序报错,中断执行。

    throw 方法被捕获以后会附带执行下一条 yield 表达式,即 附带执行一次 next 方法

    var gen = function* gen() {
      try {
        yield console.log('a')
      } catch(e) {
        // ...
      }
      yield console.log('b')
      yield console.log('c')
    }
    
    var g = gen()
    g.next() // a
    g.throw() // b
    g.next() // c
    

    这种 函数体内 捕获错误的机制大大方便了对错误的处理。对于多个 yield 表达式,可以只用一个 try...catch 代码块来捕获错误。

    thow 命令 与 g.throw 方法是无关的,两者互补影响

    Generator 函数体外抛出的错误可以在函数体内部捕获;反过来,Generator 函数体内抛出的错误也可以在函数体外的 catch 捕获。

    function* foo() {
      var x = yield 3
      var y = x.toUpperCase()
      yield y
    }
    
    var it = foo()Gen
    
    it.next()
    
    try {
      it.next(42)
    } catch(err) {
      console.log(err)
    }
    

    上面的代码,第二个next 传入 42。数值 是没有 toUpperCase 方法的,所以会抛出错误。被函数外的 catch 捕获

    一旦 Generator 执行过程中抛出错误,就不会再执行下去了。如果此后还调用 next 方法,将会返回一个 Value 属性等于 undefined,done 属性等于 true 的对象,即 JavaScript 引擎认为这个Generator 已经运行结束。

    五、Generator.prototype.return()

    Generator 函数返回的遍历器对象还有一个 return 方法,可以返回给定的值,并终结 Generator 函数的遍历。

    function* gen() {
      yield 1
      yield 2
      yield 3
    }
    
    var g = gen()
    
    g.next()
    g.return('foo')
    g.next()
    // {value: 1, done: false}
    g.return('foo')
    // {value: "foo", done: true}
    g.next()
    // {value: undefined, done: true}
    

    遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return 方法的参数 foo。同时,Generator 函数的遍历终止,返回值的 done 属性为 true。

    如果 return 方法调用时不提供参数,则返回值的 value 属性为 undefined。

    g.return() // {value: undefined, done: true }
    

    如果 Generator 函数内部有 try...finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。

    function* numbers() {
      yield 1
      try {
        yield 2
        yield 3
      } finally {
        yield 4
        yield 5
      }
      yield 6
    }
    
    var g = numbers()
    g.next()
    // {value: 1, done: false}
    g.next()
    // {value: 2, done: false}
    g.return(7)
    // {value: 4, done: false}
    g.next()
    // {value: 5, done: false}
    g.next()
    // {value: 7, done: true}
    g.next()
    // {value: undefined, done: true}
    

    如上,调用 return 方法后就开始执行 finally 代码块,然后等到 finally 代码块执行完再执行 return 方法。

    六、yield* 表达式

    如果 Generator 函数内部调用 另一个 Generator 函数,默认情况下是没有效果的。

    function* foo() {
      yield 'a'
      yield 'b'
    }
    
    function* bar() {
      yield 'x'
      foo()
      yield 'y'
    }
    
    for (let v of bar()) {
      console.log(v)
    }
    // x
    // y
    

    这时候需要用到 yield* 语句,用来在一个 Generator 函数里面执行另一个 Generator 函数

    function* bar() {
      yield 'x'
      yield* foo()
      yield 'y'
    }
    
    // 等同于
    function* bar() {
      yield 'x'
      yield 'a'
      yield 'b'
      yield 'y'
    }
    
    // 等同于
    function* bar() {
      yield 'x'
      for(let v of foo()) {
        yield v
      }
      yeild 'y'
    }
    
    for(let v of bar()) {
      console.log(v)
    }
    // 'x'
    // 'a'
    // 'b'
    // 'y'
    

    下面是一个对比的例子

    function* inner() {
      yield 'hello!'
    }
    
    function* outer1() {
      yield 'open'
      yield inner()
      yield 'close'
    }
    
    function* outer2() {
      yield 'open'
      yield* inner()
      yield 'close'
    }
    
    // yield
    var gen1 = outer1()
    gen1.next().value // 'open'
    gen1.next().value // 返回一个遍历器对象
    gen1.next().value // 'close'
    
    // yield*
    var gen2 = outer2()
    gen2.next().value // 'open'
    gen2.next().value // 'hello'
    gen2.next().value // 'close'
    

    从语法的角度看,如果 yield 命令后面跟的是一个遍历器对象,那么需要在 yield 命令后面加上星号,表明返回的是一个遍历器对象。这被称为 yield* 语句。

    let delegatedIterator = (function* () {
      yield 'Hello!'
      yield 'Bye!'
    }())
    
    let delegatingIterator = (function* () {
      yield 'Greetings'
      yield* delegatedIterator
      yield 'Ok, bye'
    }())
    
    for (let value of delegatingIterator) {
      console.log(value)
    }
    // Greetings
    //  Hello!
    // Bye!
    // Ok, bye
    

    yield* 后面的 Generator 函数(没有 return 语句是)等同于在 Generator 函数内部部署一个 for...of 循环。

    function* concat(iter1, iter2) {
      yield* iter1
      yield* iter2
    }
    
    // 等同于
    function* concat(iter1, iter2) {
      for (var value of iter1) {
        yield value
      }
      for (var value of iter2) {
        yield value
      }
    }
    

    反之,在有 return 语句是则需要用 var value = yield* iterator 的形式获取 return 语句的值

    如果 yield* 后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

    实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。

    let read = (function* () {
      yield 'hello'
      yield* 'hello'
    })()
    
    read.next().value // 'hello'
    read.next().value // 'h'
    

    yield 语句返回整个字符串,yield* 语句返回单个字符。因为字符串具有 Iterator 接口,所以用 yield* 遍历。

    如果被代理的 Generator 函数有return 语句,那么便可以向代理它的 Generator 函数返回数据。

    function* foo() {
      yield 2
      yield 3
      return 'foo'
    }
    
    function* bar() {
      yield 1
      var v = yield* foo()
      console.log('v:' + v)
      yield 4
    }
    
    var it = bar()
    
    it.next().value // 1
    it.next().value // 2
    it.next().value // 3
    it.next().value
    // "v: foo"
    // 4
    it.next().value // undefined
    

    yield* 命令可以很方便地取出嵌套数组的成员。

    function* iterTree(tree) {
      if (Array.isArray(tree)) {
        for(let i = 0, len = tree.length; i < len; i++) {
          yield* iterTree(tree[i])
        }
      } else {
        yield tree
      }
    }
    
    const tree = ['a', ['b', 'c'], ['d', 'e']]
    
    for (let x of iterTree(tree)) {
      console.log(x)
    }
    // a
    // b
    // c
    // d
    // e
    

    七、作为对象属性的 Generator 函数

    如果一个对象的属性时 Generator 函数,那么可以简写成下面的形式

    let obj = {
      * myGeneratorMethod() {
        // ...
      }
    }
    
    // 等价于
    let obj = {
      myGeneratorMethod: function* () {
        // ...
      }
    }
    

    八、Generator 函数 this

    Generator 函数总是返回一个遍历器,ES6规定这个遍历器是 Generator 函数的实例,它也继承了 Generator 函数的 prototype 对象上的方法。

    function* g() {}
    
    g.prototype.hello = function() {
      return 'hi!'
    }
    
    let obj = g()
    
    obj instanceof g // true
    obj.hello() // 'hi'
    

    如果把 g 当作普通的构造函数,则并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。Generator 函数也不能 更 new 命令一起用,否则会报错。

    如何让 Generator 函数返回一个正常的对象实例:
    首先,生成一个空对象,使用 call 方法绑定Generator 函数内部的 this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。

    function* F() {
      this.a = 1
      yield this.b = 2
      yield this.c = 3
    }
    var obj = {}
    var f = F.call(obj)
    
    f.next()
    // {value: 2, done: false}
    
    obj.a // 1
    obj.b // 2
    

    上面的代码执行的是遍历器对象 f ,但是生成的对象实例是 Obj,将这两个对象统一的一个方法是将 obj 换成 F.prototype

    function* F() {
      this.a = 1
      yield this.b = 2
      yield this.c = 3
    }
    var f = F.call(F.prototype)
    
    f.next()
    // {value: 2, done: false}
    
    f.a // 1
    f.b // 2
    

    再将 F 改成构造函数,就可以对它执行 new 命令了。

    function* gen() {
      this.a = 1
      yield this.b = 2
      yield this.c = 3
    }
    
    function F() {
      return gen.call(gen.prootype)
    }
    
    var f = new F()
    f.next()
    f.a // 1
    

    九、含义

    9.1、Generator 与状态机

    Generator 是实现状态机的最佳结构

    var ticking = true
    var clock = function() {
      if (ticking)  console.log('Tick')
      else console.log('Tock')
      ticking = !ticking
    }
    

    上面的 clock 函数一共有两种状态,每次允许,都会改变一次状态。

    如果用 Generator 实现,如下

    var clock = function* () {
      while (true) {
        console.log('Tick!')
        yield
        console.log('Tock')
        yield
      }
    }
    

    九、应用

    9.1、异步操作的同步化表达

    Generator 函数的一个重要意义就是用于处理异步操作,改写回调函数。

    function* loadUI() {
      showLoadingScreen() // 显示界面
      yield loadUIDataAsynchronously() // 加载数据
      hideLoadingScreen() // 隐藏界面
    }
    var loader = loadUI()
    // 加载 UI
    loader.next()
    
    // 卸载 UI
    loader.next()
    

    这种写法的好处是所有 loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰

    AJAX 是典型的异步操作,通过 Generator 函数部署 AJAX 操作,可以用同步的方式表示

    function* main() {
      var result = yield request('http://some.url')
      var resp = JSON.parse(result)
      console.log(resp.value)
    }
    
    function request(url) {
      makeAjaxCall(url, function(response) { // 成功的回调
        it.next(response)
      })
    }
    
    var it = main()
    it.next()
    

    makeAjaxCall 函数中的 next 方法必须加上 response 参数,因为 yield 语句构成的表达式本身是没有值的,总是等于 undefined

    9.2、控制流管理

    如果有一个多步操作非常耗时,采用回调函数可能会写成下面这样

    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          // ...
        })
      })
    })
    

    采用 Promise 改写上面的代码

    Promise.resolve(step1)
      .then(step2)
      .then(step3)
      .then(function(value4) {
        // ...
      }, function(error) {
        // ...
      })
      .done()
    

    Generator 函数可以进一步改善代码运行流程

    function* longRunningTask(value1) {
      try {
        var value2 = yield step1(value1)
        var value3 = yield step2(value2)
        var value4 = yield step3(value3)
      } catch (e) {
        // ...
      }
    }
    

    然后使用一个函数按次序自动执行所有步骤

    scheduler(longRunningTask (initialValue))
    
    function scheduler(task) {
      var taskObj = task.next(task.value)
      // 如果 Generator 函数未结束,就继续调用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task)
      }
    }
    

    上面的这种做法只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作


    利用 for...of 循环自动依次执行 yield 命令的特性,提供一种更一般的控制流程管理的方法

    let steps = [step1Func, step2Func, stop3Func]
    
    function* iterateSteps(steps) {
      for (var i=0; i < steps.length; i++) {
        var step = steps[i]
        yield step()
      }
    }
    

    数组 steps 封装了一共任务的多个步骤,Generator 函数 iterateSteps 则依次为这些步骤加上了 yield 命令

    将 任务 分解成 步骤 之后,还可以将项目分解成多个依次执行的任务。

    let jobs = [job1, job2, job3]
    
    function* iterateJobs(jobs) {
      for (var i = 0; i < jobs.length; i++) {
        var job = jobs[i]
        yield* iterateSteps(job.steps)
      }
    }
    

    最后,可以用 for...of 循环一次性执行所有任务的所有步骤

    for (var step of iterateJobs(jobs)) {
      console.log(step.id)
    }
    

    上面的做法只能用于所有步骤都是同步操作的情况。

    for...of 本质上是一共 while 循环,所以上面的代码实质上执行的是下面的逻辑。

    var it = iterateJobs(jobs)
    var res = it.next()
    
    while(!res.done) {
      var result = res.value
      // ...
      res = it.next()
    }
    

    9.3、部署 Iterator 接口

    利用 Generator 函数可以在任意对象上部署 Iterator 接口

    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
    

    上诉代码中,myObj 是一个普通对象,通过 iterEntries 函数就有了 Iterator 接口。也就是说,可以在任意对象上部署 next 方法

    下面是一共对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。

    function* makeSimpleGenerator(array) {
      var nextIndex = 0
    
      while (nextIndex < array.length) {
        yield array[nextIndex++]
      }
    }
    
    var gen = makeSimpleGenerator(['yo', 'ya'])
    
    gen.next().value // 'yo'
    gen.next().value // 'ya'
    gen.next().done // true
    

    9.4、作为数据结构

    Generator 可以看作数据结构,更准确地说,可以看作一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式提供类似数组的接口

    function* doStuff() {
      yield fs.readFile.bind(null, 'hello.txt')
      yield fs.readFile.bind(null, 'world.txt')
      yield fs.readFile.bind(null, 'and-such.txt')
    }
    

    上面的代码依次返回 3 个 函数,但是由于使用了 Generator 函数,导致可以像处理数组那样处理这3给返回的函数

    for (task of doStuff()) {
      // task 是一共函数,可以像回调函数那样使用它
    }
    

    如果用 ES5 表示,完全可以用数组模拟 Generator 的这种用法

    function doStuff() {
      return [
        fs.readFile.bind(null, 'hello.txt'),
        fs.readFile.bind(null, 'world.txt')
        fs.readFile.bind(null, 'and-such.txt')
      ]
    }
    

    上面的函数可以用一模一样的 for...of 循环处理。两相比较不难看出,Generator 使得数据或操作具备了类似数组的接口。

    相关文章

      网友评论

          本文标题:Generator 函数

          本文链接:https://www.haomeiwen.com/subject/ttcrmctx.html