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

整個過程多麼的巧妙啊。。。。。。。。。

相關文章
相關標籤/搜索