第十六章 Generator函数的语法

2018-12-21  本文已影响0人  A郑家庆

基本概念

  Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
  Generator函数除了是状态机,还是一个遍历器对象生成函数,所以可以把Generator函数放在Symbol.iterator属性上。
  形式上,Generator函数是一个普通函数,但是有两个特征:一是function命令与函数名之间有一个星号;二是函数体内部使用yield语句定义不同的内部状态。

Generator函数是遍历器生成函数,调用Generator函数返回遍历器对象,调用遍历器对象返回成员信息,跟Iterator遍历器很相似。Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。

yield表达式

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

let arr = [1, [[2, 3], 4], [5, 6]]
let flat = function* (a) {
    let length = a.length
    for (let i = 0; i < length; i++) {
        let item = a[i]
        if (typeof item !== 'number') {
            yield* flat(item)
        } else {
            yield item
        }
    }
}
for (let f of flat(arr)) {
    console.log(f)
}
// 1 2 3 4  5 6

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

function* demo () {
    console.log('Hello' + yield)         // 报错
    console.log('Hello' + yield 123)     // 报错

   console.log('Hello' + (yield))      // 正确
   console.log('Hello' + (yield 123))  // 正确

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

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

与Iterator接口的关系

由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。

let myIterator = {}
myIterator[Symbol.iterator] = function* () {
     yield 1
     yield 2
     yield 3
}
[...myIterator]    // [1,2,3]

上面代码中,Generator函数赋值给Symbol.iterator属性,从而使得myIterator对象具有了Iterator接口,可以被扩展运算符遍历。
Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

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

yield表达式只能用在Generator函数里面,用在其他地方会报错。

next方法的参数

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

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

  上面的代码先定义了一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行到yield语句时,变量reset的值总是undefined。当next方法带有一个参数true时,当前的变量reset就被重置为这个参数(即true),因而i会等于-1,下一轮循环就从-1开始递增。
  这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,其上下文状态(context)是不变的。通过next方法的参数就有办法在Generator函数开始运行后继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段从外部向内部注入不同的值,从而调整函数行为。

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

  上面代码中运行next方法不带参数,导致yield返回值为undefined,所以导致y为NaN,z为NaN。
  如果向next方法提供参数,返回结果就完全不一样了。上面的代码第一次调用b值时,返回的是6,第二次调用next方法,将上一次yield语句的值设为12,因此y的值等于24,返回8,第三次调用next方法,将上一次yield语句的值设为13,因此z等于13,return的值为42。

for...of循环

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

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

一旦next方法的返回对象的done值为true,for...of循环就会终止,且不包含该返回对象。
原生js对象没有遍历接口,无法被for...of循环,通过Generator函数为它加上这个接口后就可以用了。

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

上面代码遍历的是Iterator对象,不是jane对象,因为jane对象不具备Iterator接口,无法用for...of遍历。这时,我们通过Generator函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。

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

Generator.prototype.throw()

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

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

上面代码中,遍历器对象i连续抛出两个错误,第一个错误被Generator函数体内的catch语句捕获。第二次抛出错误,由于Generator函数内部的catch语句已经执行过了,不会再捕获到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。不要混搅遍历器对象的throw方法和全局的throw命令。上面的错误是用遍历器对象throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。

Generator.prototype.return()

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

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

如果return方法调用时不提供参数,则返回值的value属性为undefined。
如果Generator函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行。

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

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

yield*表达式

直接在Generator函数内部调用另一个Generator,是没有效果的,所以需要用到yield*语句,用来在一个Generator函数里面执行另一个Generator函数。

function* foo () {
    yield 'a'
    yield 'b'
}
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
    }
    yield 'y'
}
for (let i of bar()) {
     console.log(i)
}
//  x a b y

yield后面的Generator函数(没有return语句时)等同于在Generator函数内部部署了一个for...of循环。实际上,任何数据结构只要有Iterator接口,就可以被yield遍历。

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

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

function* foo () {
    yield 2
    yield 3
    return 'foo'
}
function* bar () {
    yield 1
    let v = yield* foo()
    console.log('v' + v)
    yield 4
}
let it = bar()
it.next()     // {value: 1, done: false}
it.next()     // {value:2, done: false}
it.next()     // v: 'foo'  {value: 4, done: false}
it.next()     // {value: undefined, done: true}

yield*命令可以很方便地取出嵌套数组的所有成员(类似递归)。

function* iterTree(tree) {
    if (Array.isArray(tree)) {
         for (let i = 0; i < tree.length; 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 = {
    * myGeneratorFun () {
     ...
     }
}
// 等同于
let obj = {
    myGeneratorFun: 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'

上面的代码表明,Generator函数g返回的遍历器obj是g的实例,而且继承了g.prototype。但是如果把g当作普通的构造函数,则不会生效,因为g返回的总是遍历器对象,而不是this对象。

function* g() {
    this.a = 11
}
let obj = g()
obj.a    // undefined

Generator函数也不能跟new命令一起用,否则会报错。

function* F() {
    yield this.x = 2
    yield this.y = 3
}
new F()  // F is not a constructor

上面代码中,new命令跟构造函数F一起使用,结果报错,因为F不是构造函数。

含义

Generator与状态机

Generator是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。

let ticking = true
let clock = function () {
    if (ticking) {
         console.log('tick')
    } else {
         console.log('tock')
   }
   ticking = !ticking
}

上面的clock函数一共有两种状态(tick和tock),每运行一次,就改变一次状态。这个函数用Generator实现,代码如下:

let clock = function* () {
     while (true) {
          console.log('tick')
          yield
          console.log('tock')
          yield
     }
}

上面代码对比我们发现,少了用来保存状态的外部变量ticking,因为Generator函数不需要外部变量保存状态,它本身就包含了状态信息,即目前是否为暂停状态。

Generator与协程

协程是一种程序运行的方式,可以理解成"协作的线程"或"协作的函数"。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

协程与子例程的差异

传统的"子例程"采用堆栈式"后进先出"的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下即多个函数)可以并行执行,但只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态,线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权时再恢复执行。这种可以并行执行、交换执行权的线程(或函数)就成为协程。
从实现上看,在内存中子例程只是用一个栈,而协程是同时存在多个栈,但只有一个栈是在运行态。也就是说,协程是以多占用内存为代价实现多任务的并行运行。

协程与普通线程的差异

不难看出,协程适用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文,可以分享全局变量。它们的不同之后在于,同一时间可以有多个线程,但是运行的协程只能有一个,其他协程都处于暂停态。此外,普通的线程是抢占式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于javascript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务都可以保持自己的调用栈。这样做最大的好处就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错原始的调用栈早就结束。
Generator函数对协程不完全实现,所以被成为"半协程",意思是只有Generator函数的调用者才能将执行权还给Generator函数。如果完全实现协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权。

应用

异步操作的同步化表达

Generator函数的暂停执行效果,意味着可以把异步操作写在yield语句里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面,反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用于处理异步操作,改写回调函数。

function* loadUi () {
    let result = yield loadUiAsync()
    let resp = JSON.parse(result)
    console.log(result.value)
}
function loadUiAsync () {
     makeAjaxCall('http://some.url', function(response) {
            it.next(response)
     })
}
let it = loadUi()
it.next()

之前用Promise写异步需要用reject方法触发then中第一个回调函数,现在只需要调用next方法就可以执行,回调函数可以写在外面。
注意:for...of本质上是一个while循环

let it = iterateJobs(jobs)
let res = it.next()
while (!res.done) {
    let result = res.value
    res = it.next()
}
上一篇下一篇

猜你喜欢

热点阅读