【JS】JavaScript異步系列(4)——生成器

本篇博客來源於王福明博客的異步系列和《你不知道的JavaScript》
原博客地址爲:http://www.cnblogs.com/wangfupeng1988/p/6532713.html

1. 打破完整運行

在前面已經學習了promise的解決方法來處理異步操作的代碼邏輯,接下來我們來看一種順序、看似同步的異步流程控制表達風格——ES6的生成器(generator)。

先看一段代碼:

var x = 1;
function* foo(){
    x++;
    yield;//暫停!
    console.log("x:",x);
}

function bar(){
    x++;
}

//構造一個迭代器it來控制這個生成器
var it = foo();

//這裏啓動foo()!
it.next();
console.log(x); //2
bar();
console.log(x); //3
it.next();  //x: 3

這段代碼的運行過程是什麼樣呢?請往下看:

  1. it = foo()運算並沒有執行生成器foo(),,而只是夠早了一個迭代器(iterator),這個迭代器會控制它的執行。【後面會接受迭代器】
  2. 第一個it.next()啓動了生成器foo(),並運行了foo()第一行的x++foo()yield語句處暫停,到這裏第一個it.next()調用結束。此時的foo()仍在運行並且是活躍的,但處於暫停狀態。
  3. 此時查看x的值爲2.然後我們再調用bar(),再次遞增x。
  4. 再次查看x的值,此時x的值爲3.
  5. 最後的it.next()調用從暫停處恢復了生成器foo()的執行,並運行console.log(...)語句,這條語句使用當前x的值3.

顯然,foo()啓動了,但還沒有完整運行,它在yield處暫停了。後面恢復了foo()並讓它運行到結束。

1.1 輸入和輸出

生成器函數實際是特殊的函數,它也可以接受參數(輸入),也能夠返回值(輸出)。

function* foo(x,y){
    return x * y;
}

var it = foo(6,7);
var res = it.next();
res.value;  //42

我們向foo()傳入實參6和7分別作爲參數x和y。foo()向調用代碼返回42.

迭代消息傳遞

處理能夠接受參數並提供返回值之外,生成器還提供了更強大的內建消息輸入輸出能力,通過yieldnext(...)實現。

function* foo(x){
    var y = x*(yield);
    return y;
}

var it = foo(6);

//啓動foo(...)
it.next();
var res = it.next(7);
res.value;  //42

首先,傳入6作爲參數x。然後調用it.next(),這會啓動foo().

foo(...)內部,開始執行語句var y = x …,但隨後就遇到一個yield表達式。他就會在這點上暫停foo(...)(在賦值語句中間暫停!)並在本質上要求調用代碼爲yield表達式提供一個結果值。接下來,調用it.next(7),這句把值7傳回作爲被暫停的yield表達式的結果。
所以,這時賦值語句實際上就是var y = 6 * 7。現在,return y返回值42作爲調用it.next(7)的結果。

多個迭代器

關於多個迭代器的,和前面的理解一樣。看如下代碼:

function* foo(){
    var x = yield 2;
    z++;
    var y = yield (x*z);
    console.log(x,y,z);
}

var z = 1;

var it1 = foo();
var it2 = foo();

var val1 = it1.next().value;    //2 <--yield 2
var val2 = it2.next().value;    //2 <--yield 2

val1 = it1.next(val2*10).value; //40 <-- x:20,z:2
val2 = it2.next(val1*5).value;  //600 <-- x:200,z:3

it1.next(val2/2);   //y:300
                    // 20 300 3
it2.next(val1/4);   //y:10
                    // 200 10 3

能看出正確結果麼?

2、Iterator 迭代器

前面介紹了生成器的一種有趣用法是作爲一種產生值的方式。接下來,介紹一點迭代器

2.1 Symbol數據類型簡介

Symbol是一個特殊的數據類型,和number string等並列,詳細可以看阮一峯老師的ES6入門中的介紹。
現在先記住,Symbol數據類型也可以作爲對象屬性的key。看如下代碼:

var obj = {}
obj.a = 100
obj[Symbol.iterator] = 200
console.log(obj)  // {a: 100, Symbol(Symbol.iterator): 200}

[Symbol.iterator]是一個特殊的數據類型——Symbol類型,但是也可以像number string類型一樣,作爲對象的屬性key來使用。

2.2 iterable

iterable(可迭代):指一個包含可以在其值上迭代的迭代器對象。

從ES6開始,從一個iterable中提取迭代器的方法是:iterable必須支持一個函數,其名稱是專門的ES6符號值Symbol.iterator。調用這個函數時,它會返回一個迭代器。

在ES6中,元素具有[Symbol.iterator]屬性數據類型的有:數組、某些類似數字的對象(arguments等)、Set和Map。

原生具有[Symbol.iterator]屬性的數據類型有一個特點:可以使用for...of來取值。

var item
for (item of [100, 200, 300]) {
    console.log(item)
}
// 打印出:100 200 300 
// 注意,這裏每次獲取的 item 是數組的 value,而不是 index ,這一點和 傳統 for 循環以及 for...in 完全不一樣
// for..of循環自動調用它的Symbol.iterator函數來構建一個迭代器

2.3 生成Iterator對象

首先定義一個數組,並生成該數組的Iterator對象

const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]()  // 通過執行 [Symbol.iterator] 的屬性值(函數)來返回一個 iterator 對象

生成了iterator,該如何使用呢?有兩種方式:nextfor...of
1. next

console.log(iterator.next())  // { value: 100, done: false }
console.log(iterator.next())  // { value: 200, done: false }
console.log(iterator.next())  // { value: 300, done: false }
console.log(iterator.next())  // { value: undefined, done: true }
  1. for…of
let i
for (i of iterator) {
    console.log(i)
}
// 打印:100 200 300 

2.4 Generator返回的也是Iterator對象

現在,你應該明白了,我們在第一部分說的生成器,就是生成一個Iterator對象。因此會有next(),也可以通過for...of來遍歷。

function* Hello() {
    yield 100
    yield (function () {return 200})()
    return 300 
}
const h = Hello()
console.log(h[Symbol.iterator])  // function [Symbol.iterator](){[native code]}

執行const h = Hello()得到的就是一個iterator對象,因爲h[Symbol.iterator]是有值的。既然是iterator對象,那麼就可以使用next()和for…of進行操作

console.log(h.next())  // { value: 100, done: false }
console.log(h.next())  // { value: 200, done: false }
console.log(h.next())  // { value: 300, done: false }
console.log(h.next())  // { value: undefined, done: true }

let i
for (i of h) {
    console.log(i);//100 200
}

3、生成器的應用

3.1 yield* 語句

如果有兩個Generator,想要在第一個中包含第二個,如下需求:

function* G1() {
    yield 'a'
    yield 'b'
}
function* G2() {
    yield 'x'
    yield 'y'
}

針對以上兩個Generator,我的需求是:一次輸出a x y b,該如何做?
for..of解決:

var g1 = G1();
var g2 = G2();

for(var i of g1){
    console.log(i);
    for(var j of g2){
       console.log(j);
    }
 }

但是,更簡潔的方式yield*表達式

function* G1() {
    yield 'a'
    yield* G2()  // 使用 yield* 執行 G2()
    yield 'b'
}
function* G2() {
    yield 'x'
    yield 'y'
}
for (let item of G1()) {
    console.log(item)
}

yield*後面會接一個Generator,而且會把它其中的yield按照規則來一步一步執行。

4、異步迭代生成器

生成器與異步編碼模式及解決回調問題等有什麼關係呢?接下來我們就來看這個問題。
先看一段代碼:

function foo(x, y, cb) {
        ajax(
            "http://some.url.1/?x="+x+"&y="+y,
            cb
        );
    }

    foo(11,31,function (err, text) {
        if(err){
            console.log(err);
        }
        else{
            console.log(text);
        }
    });

若想要通過生成器來表達烔煬的任務流程控制,可以這樣實現:

function foo(x, y) {
        ajax(
            "http://some.url.1/?x="+x+"&y="+y,
            function (err, data) {
                if(err){
                    //向*main()拋出一個錯誤
                    it.throw(err) ;
                }
                else{
                    //用收到的data恢復*main()
                    it.next(data);
                }

            }
        );
    }

    function* main() {
        try{
            var text = yield foo(11,31);
            console.log(text);
        }
        catch(err){
            console.log(err);
        }
    }

    var it =main();
    //啓動生成器
    it.next();

第一眼看上去,與之前的回調代碼對比起來,代碼更長了,但是,別想得這麼簡單!

yield foo(11,31)中,首先調用foo(11,31),它沒有返回值(返回undefined),所以我們發出了一個調用來請求數據,但實際上之後做的是yield undefined。因爲這段代碼當前不依賴yield出來的值來做任何事。

這裏yield只是將其用於流程控制實現暫停/阻塞。它的消息傳遞,只是生成器恢復運行之後的單向消息傳遞。

總結一下:我們在生成器內部有了看似完全同步的代碼(出來yield),但隱藏在背後的是,在foo(..)內的運行可以完全異步。

這樣,對於回調無法以順序同步的、符合我們大腦思考模式的方式表達異步這個問題,是一個近乎完美的解決方案。

同步錯誤處理

try{
            var text = yield foo(11,31);
            console.log(text);
        }
        catch(err){
            console.log(err);
        }

在前面我們已經看到yield是如何讓賦值語句暫停來等待foo(...)完成,使得響應完成後可以被賦給text。精彩部分在於yield暫停也使得生成器能夠捕獲錯誤。
通過這段代碼,把錯誤拋出到生成器中:

 if(err){
    //向*main()拋出一個錯誤
    it.throw(err) ;
 }

還可以捕獲通過throw(..)拋入生成器的同一個錯誤,基本上也就是給生成器一個處理它的機會;如果生成器沒有處理的話,迭代器代碼就必須處理:

function* main() {
        var x = yield "Hello World";

        //永遠不會到達這裏
        console.log(x);
    }

    var it = main();
    it.next();

    try{
        // *main()會處理這個錯誤嗎?
        it.throw("Oops");
    }
    catch (err) {
        //不行,沒有處理!
        console.log(err);   //Oops
    }

5、生成器+promise

這個看個簡單的例子吧,感覺更偏向於結構設計了,一時半會兒沒有實際應用感覺說不清楚,以後再細說吧!
簡單的例子:

function foo(x, y) {
        return request(
            "http://some.url.1/?x=" + x + "&y=" + y
        );
    }

    function* main() {
        try{
            var text = yield foo(11,31);
            console.log(text);
        }
        catch (err) {
            console.log(err);
        }
    }

我們把promise的實現細節抽象出來了,在生成器中只進行函數調用就行了。實現了promise的隱藏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章