3

翻译自

github

概述

  1. 什么是generators?

我们可以把generators理解成一段可以暂停并重新开始执行的函数

function* genFunc() {
    // (A)
    console.log('First');
    yield; //(B)
    console.log('Second'); //(C)
}

function*是定义generator函数的关键字,yield是一个操作符,generator 可以通过yield暂停自己执行,另外,generator可以通过yield接受输入和对外输入

当我们调用genFunc(),我们得到一个generator对象genObj,我们可以通过这个genObj控制程序的执行

const genObj = genFunc()

上面的程序初始会暂停在行A,调用genObj.next()会使程序继续执行直到遇到下一个yield

> genObj.next();
First
{ value: undefined, done: false }

这里先忽略genObj.next()返回的对象,之后会介绍

现在,程序暂停在了行B,再次调用 genObj.next(),程序又开始执行,行C被执行

> genObj.next()
Second
{ value: undefined, done: true }

然后,函数就执行结束了,再次调用genObj.next()也不会有什么效果了

  1. generator能扮演的角色

generators 可以扮演三种角色

  • 迭代器(数据生产者)

每一个yield可以通过next()返回一个值,这意味着generators可以通过循环或递归生产一系列的值,因为generator对象实现了Iterable接口,generator生产的一系列值可以被ES6中任意支持可迭代对象的结构处理,两个例子,for of循环和扩展操作(...)

  • 观察者(数据消费者)

yield可以通过next()接受一个值,这意味着generator变成了一个暂停执行的数据消费者直到通过next()给generator传递了一个新值

  • 协作程序(数据生产者和消费者)

考虑到generators是可以暂停的并且可以同时作为数据生产者和消费者,不会做太多的工作就可以把generator转变成协作程序(合作进行的多任务)

下面详细介绍这三种

generators作为数据生产者(iterators)

generators同时实现了接口Iterable 和 Iterator(如下所示),这意味着,generator函数返回的对象是一个迭代器也是一个可迭代的对象

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

generator对象完整的接口后面会提到,这里删掉了接口Iterable的return()方法,因为这个方法这一小节用不到

generator函数通过yield生产一系列的值,这些值可以通过迭代器的next()方法来使用,例如下面的generator函数生成了值a和b

function* genFunc(){
    yield 'a'
    yield 'b'
}

交互展示如下

> const genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }

> genObj.next()
{ value: 'b', done: false }

> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }
  1. 迭代generator的三种方式

    • for of循环
       for (const x of genFunc()) {
           console.log(x);
       }
       // Output:
       // a
       // b
  • 扩展操作符(...)
const arr = [...genFunc()]; // ['a', 'b']
  • 解构赋值
> const [x, y] = genFunc();
> x
'a'
> y
'b'
  1. generator中的return

上面的generator函数没有包含一个显式的return,一个隐式的return 返回undefined,让我们试验一个显式返回return的generator

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'result';
}

下面的结构表明return 指定的值保存在最后一个next()返回的对象中

> const genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }

然而,大部分和可迭代对象一起工作的结构会忽略done属性是true的对象的value值

for (const x of genFuncWithReturn()) {
    console.log(x);
}
// Output:
// a
// b

const arr = [...genFuncWithReturn()]; // ['a', 'b']

yield*会考虑done属性为true的value值,后面会介绍

  1. generator函数中抛异常

如果一个异常离开了generator函数,next()可以抛出它

function* genFunc() {
    throw new Error('Problem!');
}
const genObj = genFunc();
genObj.next(); // Error: Problem!

这意味着next()可以生产三种类型的值

  • 对于可迭代序列中的一项x,它返回 {value:x,done:false}
  • 对于可迭代序列的最后一项,明确是return返回的z,它返回{value:z,done:true}
  • 对于异常,它抛出这个异常
  1. 通过 yield*递归

我们只能在generator函数中使用yield,如果我们想通过generator实现递归算法,我们就需要一种方式来在一个generator中调用另一个generator,这就用到了yield*,现在,我们只介绍yield*用在generator函数产生值的情况,之后介绍yield*用在generator接受值的情况

generator递归调用另一个generator的方式

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

function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}

执行结构

const arr = [...bar()];
//['x', 'a', 'b', 'y']

在内部,yield*像下面这样工作的

function* bar() {
    yield 'x';
    for (const value of foo()) {
        yield value;
    }
    yield 'y';
}

另外,yield*的操作数不一定非得是一个generator函数生成的对象,可以是任何可迭代的

function* bla() {
    yield 'sequence';
    yield* ['of', 'yielded'];
    yield 'values';
}
const arr = [...bla()];
// ['sequence', 'of', 'yielded', 'values']

yield*考虑可迭代对象的最后一个值

ES6中的很多结构会忽略generator函数返回的可迭代对象的最后一个值(例如 for of,扩展操作符,如上面介绍过的那样),但是,yield*的结果是这个值

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'The result';
}
function* logReturned(genObj) {
    const result = yield* genObj;
    console.log(result); // (A)
}

执行结果

> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]

generators作为数据消费者(observers)

作为数据的消费者,generator函数返回的对象也实现了接口Observer

interface Observer {
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

作为observer,generator暂停执行直到它接受到输入值,这有三种类型的输入,通过以下三种observer接口提供的方法

  • next() 发送正常的输入
  • return() 终止generator
  • throw() 发送一个错误
  1. 通过next()发送值

function* dataConsumer() {
    console.log('Started');
    console.log(`1. ${yield}`); // (A)
    console.log(`2. ${yield}`);
    return 'result';
}

首先得到generator对象

const genObj = dataConsumer();

然后执行genObj.next(),这会开始这个generator.执行到第一个yield处然后暂停。此时next()的结果是yield在行A产出的值(是undifined,因为这地方的yield后面没有操作数)

> genObj.next()
//Started
{ value: undefined, done: false }

然后再调用next()两次,第一次传个参数'a',第二次传参数'b'

> genObj.next('a')
//1. a
{ value: undefined, done: false }

> genObj.next('b')
//2. b
{ value: 'result', done: true }

可以看到,第一个next()调用的作用仅仅是开始这个generator,只是为了后面的输入做准备

可以封装一下

function coroutine(generatorFunction) {
    return function (...args) {
        const generatorObject = generatorFunction(...args);
        generatorObject.next();
        return generatorObject;
    };
}

使用

const wrapped = coroutine(function* () {
    console.log(`First input: ${yield}`);
    return 'DONE';
});

> wrapped().next('hello!')
First input: hello!
  1. return() 和 throw()

generator对象有两个另外的方法,return()和throw(),和next()类似

让我们回顾一下next()是怎么工作的:

  1. generator暂停在yield操作符
  2. 发送x给这个yield
  3. 继续执行到下一个yield,return或者throw:

    • yield x 导致 next() 返回 {value: x, done: false}
    • return x 导致 next() 返回 {value:x, done:true}
    • throw err 导致 next() 抛出err

return()和throw() 和next()类似工作,但在第二步有所不同

  • return(x) 在 yield的位置执行 return x
  • throw(x) 在yield的位置执行throw x

return()终止generator

return() 在 yield的位置执行return

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } finally {
        console.log('Exiting');
    }
}

> const genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.return('Result')
Exiting
{ value: 'Result', done: true }

阻止终止

我们可以阻止return()终止generator如果yield是在finally块内(或者在finally中使用return语句)

function* genFunc2() {
    try {
        console.log('Started');
        yield;
    } finally {
        yield 'Not done, yet!';
    }
}

这一次,return()没有退出generator函数,当然,return()返回的对象的done属性就是false

> const genObj2 = genFunc2();

> genObj2.next()
Started
{ value: undefined, done: false }

> genObj2.return('Result')
{ value: 'Not done, yet!', done: false }

可以再执行一次next()

> genObj2.next()
{ value: 'Result', done: true }

发送一个错误

throw()在yield的位置抛一个异常

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } catch (error) {
        console.log('Caught: ' + error);
    }
}
> const genObj1 = genFunc1();

> genObj1.next()
Started
{ value: undefined, done: false }

> genObj1.throw(new Error('Problem!'))
Caught: Error: Problem!
{ value: undefined, done: true }
  1. yield* 完整的故事

到目前为止,我们只看到以yield的一个层面: 它传播生成的值从被调用者到调用者。既然我们现在对generator接受值感兴趣,我们就来看一下yield的另一个层面:yield*可以发送调用者接受的值给被调用者。在某种程度上,被调用者变成了活跃的generator,它可以被调用者生成的对象控制

function* callee() {
    console.log('callee: ' + (yield));
}
function* caller() {
    while (true) {
        yield* callee();
    }
}
> const callerObj = caller();

> callerObj.next() // start
{ value: undefined, done: false }

> callerObj.next('a')
callee: a
{ value: undefined, done: false }

> callerObj.next('b')
callee: b
{ value: undefined, done: false }

generators作为协同程序(协作多个任务)

这一节介绍generator完整的接口(组合作为数据生产者和消费者两种角色)和一个同时要使用这两种角色的使用场景:协同操作多任务

  1. 完整的接口

interface Generator {
    next(value? : any) : IteratorResult;
    throw(value? : any) : IteratorResult;
    return(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

接口Generator结合了我们之前介绍过的两个接口:输出的Iterator和输入的Observer

interface Iterator { // data producer
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}
  1. 合作多任务

合作多任务是我们需要generators同时处理输入和输出,在介绍generator是如何工作的之前,让我们先复习一下JavaScript当前的并行状态

js是单线程的,但有两种方式可以消除这种限制

  • 多进程: Web Worker可以让我们以多进程的方式运行js,对数据的共享访问是多进程的最大缺陷之一,Web Worker避免这种缺陷通过不分享任何数据。也就是说,如果你想让Web Worker拥有一段数据,要么发送给它一个数据的副本,要么把数据传给它(这样之后,你就不能再访问这些数据了)
  • 合作多任务:有不同的模式和库可以尝试进行多任务处理,运行多个任务,但每次只执行一个任务。每个任务必须显式地挂起自己,在任务切换发生时给予它完全的控制。在这些尝试中,数据经常在任务之间共享。但由于明确的暂停,几乎没有风险。

通过generators来简化异步操作

一些基于Promise的库通过generator来简化了异步代码,generators作为Promise的客户是非常理想的,因为它们可以暂停直到结果返回

下面的例子表明co是如何工作的

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

注意这段代码看起来是多么的同步啊,虽然它在行A处执行了一个异步调用。

使用generators对co的一个简单的实现

function co(genFunc) {
    const genObj = genFunc();
    step(genObj.next());

    function step({value,done}) {
        if (!done) {
            // A Promise was yielded
            value
            .then(result => {
                step(genObj.next(result)); // (A)
            })
            .catch(error => {
                step(genObj.throw(error)); // (B)
            });
        }
    }
}

这里忽略了next()(行A)和throw()(行B)可以回抛异常

借助上面的使用分析一下:

首先得到generator对象

const genObj = genFunc();

然后将genObj.next()的返回值传递给step方法

step()中获取到value和done,如果generator没有执行完,当前的value就是上面使用中定义的promise

等到promise执行完,然后将结果result传递给generator函数

genObj.next(result)

然后在generator中程序继续往下执行

const [croftStr, bondStr] = yield XXXX
.
.
.
.

注意行A处递归调用step(genObj.next(result)),使得generator函数中可以存在多个异步调用,而co都能处理

整个过程多么的巧妙啊。。。。。。。。。


对对对就是这
556 声望47 粉丝