es6 Generators详解

翻译自html

githubgit

概述

  1. 什么是generators?

咱们能够把generators理解成一段能够暂停并从新开始执行的函数es6

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

function*是定义generator函数的关键字,yield是一个操做符,generator 能够经过yield暂停本身执行,另外,generator能够经过yield接受输入和对外输入github

当咱们调用genFunc(),咱们获得一个generator对象genObj,咱们能够经过这个genObj控制程序的执行算法

const genObj = genFunc()

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

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

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

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

> 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都能处理

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

相关文章
相关标签/搜索