ES6 Generator與異步的同步書寫

開始前

咱們歷來沒有中止過對javascript語言異步調用方式的改造,咱們一直都想用像java那樣同步的方式去寫異步,儘管Promise可讓咱們將異步回調添加到then方法中,可是這種調用方式仍然不那麼優雅,es6 中新增長了generator,咱們能夠經過他的特性來實現異步任務更加優雅的書寫方式。javascript

協程介紹

協程其實和線程,進程是沒有關係的,它不是操做系統爲咱們提供的api接口,而是經過編程語言或者彙編語言對程序上下文、程序棧來操做實現的。一個線程裏面能夠包含多個協程,線程的調度是由操做體統來決定的,協程的調度是由用戶來決定的。操做系統對其一無所知,由於能夠由用戶來調度,因此用來執行協做式的任務特別方便。(注意這裏是方便,由於能經過協程解決的問題,經過線程和進程也能夠解決,可是複雜)java

Generator介紹

Generator 是協程在es6中的實現。它在es6中是一個函數,這個函數能夠分階段執行,也就是說咱們能夠在這個函數中的某個位置選擇交出當前線程的執行權限,也能夠在當前函數外面的某個位置選擇將權限再交回這個函數,讓它繼續執行,這種調度徹底由用戶決定。在es6中協程函數是這樣的node

function* gen(p) {
    var a = yield p + 1;  //1
    var b = yield p + 2;  //2
    return b;  //3
}

var g = gen(1);
g.next();  //{value: 2, done: false}
g.next();  //{value: 3, done: false}
g.next();  //{value: undefined, done: true}

經過 var g = gen(1); 僅僅是建立了一個迭代器,函數 gen 裏面的內容並無執行函數體的執行時由第一個 g.next(); 開始的 而且將 yield 所在那那條語句執行完後就會返回結果。然後面的語句並無執行。返回值是一個對象,它的第一個屬性是 yield 後面表達式的值 (p+1或者p+2的值);第二個屬性表示Generator函數是否執行完成。這裏咱們經過 yield 執行權限交出去,經過 next 將權限返回。react

function* gen(p) {
    var a = yield p + 1;  //1
    var b = yield a + 1;  //2 注意這裏是用到了 a
    return b;
}
var g = gen(1);
g.next();  //{value: 2, done: false}
g.next();  //{value: NaN, done: false} 這裏的值是 NaN
g.next();  //{value: undefined, done: true}

g.next();  //{value: 2, done: false}
g.next(2);  //{value: 3, done: false}
g.next(6);  //{value: 6, done: true}

注意這裏 //1 處 //2 處 var a = yield p + 1;這條賦值語句中 a 的值並非 p + 1的值。這條語句只是一種寫法,這裏 a 的值是咱們在第二個 next 中傳入的 2 這個很重要 b 的值也是咱們在第三個 next 中傳入的 6es6

Generator 的重要特性

由上面的內容咱們總結 3 個關於 Generator 的重要特性編程

1 經過 yield 交出執行權限,經過 next 返回執行權限
2 調用 next 會獲得一個返回值,這個值裏面包含了 yield 後面的表達式的執行結果
3 咱們能夠經過給 next 傳遞參數,而且能夠在 Generator 函數中經過上面所寫的特殊方式來引用redux

利用 Generator 的特性來實現異步代碼的同步書寫

咱們來模擬一個異步函數segmentfault

function post(url, callback) {
    setTimeout(function() {
        var data = { //模擬異步處理結果
            url:url,
            value:10
        };
        callback(data);
    }, 1000);
}

post('http://_ivenj',function(data){
    console.log(data.url);  // http://_ivenj
    console.log(data.value);  //10
});

對應上面的這個異步函數我想經過 Generator 來這樣用api

function* gen(url) {
    var data = yield post(url);  //1
    console.log(data.url);
    console.log(data.value);
}
var g = gen('http://_ivenj');
var resultG = g.next();
g.next(resultG.value);

是的,這樣寫漂亮多了,很像 java 的同步寫法。不一樣之處就是多了個 yield* ,這個無傷大雅。固然以上這樣用確定是不行的。由於 post 畢竟是個異步方法。沒有返回值.若是不能實現這樣的寫法我這半天就是在扯淡,因此經過包裝是能夠實現的。瀏覽器

經過如下兩點能夠實現以上的書寫方式

(1)我有一篇文章 react 實踐之 redux applyMiddleware方法詳解 中介紹了柯里化(Currying)這篇文章雖然是寫react的可是柯里化是獨立的,這裏就要利用柯里化的思想

(2)咱們要在回調中調用 next 來繼續執行,(這裏有人會想不是不用回調了麼,怎麼還用,請繼續看。。。)

咱們要對 post 的調用形式進行包裝

function kPost(url) {
    return function(callback) {
        post(url, callback);
    }
}

經過這個包裝,咱們就能保證調用 kPost 就會同步的獲得一個返回值

function* gen(url) {
    var data = yield kPost(url);  //1
    console.log(data.url);
    console.log(data.value);
}
//這裏執行方式會不一樣
var g = gen('http://_ivenj');
//啓動任務
var resultG1 = g.next();
var value_resultG1 = resultG1.value; //resultG1.value 必定是一個函數,由於咱們包裝了
value_resultG1(function(data){
    g.next(data);  //經過在異步的回調中調用 next 並傳遞值來確保依賴異步結果的代碼能正確執行
});

下面就是總體代碼,是上面的片斷組合,請你粘貼到瀏覽器控制檯,或者用node運行,就會看到想要的結果

function post(url, callback) {
    setTimeout(function() {
        var data = { //模擬異步處理結果
            url:url,
            value:10
        };
        callback(data);
    }, 1000);
}
function kPost(url) {
    return function(callback) {
        post(url, callback);
    }
}
function* gen(url) {
    var data = yield kPost(url);  //1
    console.log(data.url);
    console.log(data.value);
}
//這裏執行方式會不一樣
var g = gen('http://_ivenj');
//啓動任務
var resultG1 = g.next();
var value_resultG1 = resultG1.value; //resultG1.value 必定是一個函數,由於咱們包裝了
value_resultG1(function(data){
    g.next(data);
});

有人會說,怎麼不就是將異步回調轉移出來了麼,還要寫回調。這說明你尚未真正體會個中之奧妙。咱們會發現 咱們寫的異步

value_resultG1(function(data){
    g.next(data);
});

僅僅是調用了 next 進行告終果的傳遞,這裏面有共同之處,不論是哪種異步,咱們都只傳遞值。你們的處理都是同樣的。真正的業務邏輯確實是用同步的方式寫的。那麼,咱們能夠將共同的地方提取出來,寫一個通用的函數去執行這個傳值操做,這樣,咱們徹底就告別了異步,再也看不到了,好開心。co.js就是一個這種generator的執行庫。使用它是咱們只須要將咱們的 gen 傳遞給它像這樣 co(gen) 是的就這樣。下面咱們本身寫一個 co

Generator執行器

function co(taskDef) {
    //獲取迭代器  相似 java 中的外柄迭代子
    var task = taskDef();
    //開始任務
    var result = task.next();
    //調用next的遞歸函數
    function step() {
        if (!result.done) {  //若是generator沒有執行完
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }
                    result = task.next(data);  //向後傳遞當前異步處理結果
                    step();  //遞歸執行
                });
            } else {
                result = task.next(result.value);  //若是執行完了就傳遞值
                step();  //遞歸執行
            }

        }
    }
    // 啓動遞歸函數
    step();
}

經過 co 執行的完整代碼

function post(url, callback) {
    setTimeout(function() {
        var data = { //模擬異步處理結果
            url:url,
            value:10
        };
        callback(data);
    }, 1000);
}
function kPost(url) {
    return function(callback) {
        post(url, callback);
    }
}
function gen(url) {
    return function* () {
        var data = yield kPost(url);  //1
        console.log(data.url);
        console.log(data.value);
    }
}
function co(taskDef) {
    var task = taskDef();
    //開始任務
    var result = task.next();
    // 調用next的遞歸函數
    function step() {
        if (!result.done) {  //若是generator沒有執行完
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }
                    result = task.next(data);  //向後傳遞當前異步處理結果
                    step();  //遞歸執行
                });
            } else {
                result = task.next(result.value);  //若是執行完了就傳遞值
                step();  //遞歸執行
            }

        }
    }
    // 啓動遞歸函數
    step();
}
    
co(gen('http://_ivenj')); //調用方式就是這麼簡單

以上代碼執行 1s 後會拋出一個異常,而且正確打印{url: "http://_ivenj", value: 10},聰明的你必定知道爲何會拋出異常!!!

到這裏已經說明白了,而且也說完了,你會想是否是把異步包裝成Promise也能夠呢,答案是確定的,柯里化的思想只是一種實現方式,Promise 也是一種,你能夠本身去琢磨,co.js 就是將兩種方式都實現了的一個執行器。es7 中從語言層面對 Generator 進行了包裝,在es7 中咱們可使用 asyncawait更優雅的實現相似java的順序書寫方式,asyncawaitGenerator的語法糖,在es7中內置了執行器。別人都說是終極方案。

相關文章
相關標籤/搜索