首頁  >  文章  >  web前端  >  Generator在ES6中的自動執行詳解

Generator在ES6中的自動執行詳解

不言
不言轉載
2018-10-19 15:10:592168瀏覽

這篇文章帶給大家的內容是關於ES6中Generator的自動執行詳解,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

單一非同步任務

var fetch = require('node-fetch');

function* gen(){
    var url = 'https://api.github.com/users/github';
    var result = yield fetch(url);
    console.log(result.bio);
}

為了獲得最終的執行結果,你需要這樣做:

var g = gen();
var result = g.next();

result.value.then(function(data){
    return data.json();
}).then(function(data){
    g.next(data);
});

首先執行Generator 函數,取得遍歷器對象。

接著使用 next 方法,執行非同步任務的第一階段,即 fetch(url)。

注意,由於fetch(url) 會傳回一個Promise 對象,所以result 的值為:

{ value: Promise { <pending> }, done: false }

最後我們為這個Promise 物件新增一個then 方法,先將其傳回的資料格式化(data.json()),再呼叫g.next,將所得的資料傳進去,由此可以執行非同步任務的第二階段,程式碼執行完畢。

多個非同步任務

上節我們只呼叫了一個接口,那如果我們呼叫了多個接口,使用了多個yield,我們豈不是要在then 函數中不斷的嵌套下去…

所以我們來看看執行多個非同步任務的情況:

var fetch = require('node-fetch');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var r2 = yield fetch('https://api.github.com/users/github/followers');
    var r3 = yield fetch('https://api.github.com/users/github/repos');

    console.log([r1.bio, r2[0].login, r3[0].full_name].join('\n'));
}

為了獲得最終的執行結果,你可能要寫成:

var g = gen();
var result1 = g.next();

result1.value.then(function(data){
    return data.json();
})
.then(function(data){
    return g.next(data).value;
})
.then(function(data){
    return data.json();
})
.then(function(data){
    return g.next(data).value
})
.then(function(data){
    return data.json();
})
.then(function(data){
    g.next(data)
});

但我知道你肯定不想寫成這樣…

其實,利用遞歸,我們可以這樣寫:

function run(gen) {
    var g = gen();

    function next(data) {
        var result = g.next(data);

        if (result.done) return;

        result.value.then(function(data) {
            return data.json();
        }).then(function(data) {
            next(data);
        });

    }

    next();
}

run(gen);

其中的關鍵就是yield 的時候返回一個Promise 對象,給這個Promise 物件加入then 方法,當非同步操作成功時執行 then 中的 onFullfilled 函數,onFullfilled 函數中再去執行 g.next,從而讓 Generator 繼續執行,然後再回傳一個Promise,再在成功時執行g.next,然後再返回…

#啟動器函數

##在run 這個啟動器函數中,我們在then 函數中將資料格式化data.json(),但在更廣泛的情況下,例如yield 直接跟一個Promise,而非一個fetch 函數傳回的Promise,因為沒有json 方法,程式碼就會報錯。所以為了更具備通用性,連同這個例子和啟動器,我們修改為:

var fetch = require('node-fetch');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var json1 = yield r1.json();
    var r2 = yield fetch('https://api.github.com/users/github/followers');
    var json2 = yield r2.json();
    var r3 = yield fetch('https://api.github.com/users/github/repos');
    var json3 = yield r3.json();

    console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n'));
}

function run(gen) {
    var g = gen();

    function next(data) {
        var result = g.next(data);

        if (result.done) return;

        result.value.then(function(data) {
            next(data);
        });

    }

    next();
}

run(gen);
只要 yield 後跟著一個 Promise 對象,我們就可以利用這個 run 函數將 Generator 函數自動執行。

回呼函數

yield 後一定要跟著一個 Promise 物件才能保證 Generator 的自動執行嗎?如果只是一個回呼函數呢?讓我們來看個範例:

首先我們來模擬一個普通的非同步請求:

function fetchData(url, cb) {
    setTimeout(function(){
        cb({status: 200, data: url})
    }, 1000)
}
我們將這個函數改造成:

function fetchData(url) {
    return function(cb){
        setTimeout(function(){
            cb({status: 200, data: url})
        }, 1000)
    }
}
對於這樣的Generator 函數:

function* gen() {
    var r1 = yield fetchData('https://api.github.com/users/github');
    var r2 = yield fetchData('https://api.github.com/users/github/followers');

    console.log([r1.data, r2.data].join('\n'));
}
如果要得到最終的結果:

var g = gen();

var r1 = g.next();

r1.value(function(data) {
    var r2 = g.next(data);
    r2.value(function(data) {
        g.next(data);
    });
});
如果寫成這樣的話,我們會面臨跟第一節同樣的問題,那就是當使用多個yield 時,程式碼會循環嵌套起來…

同樣利用遞歸,所以我們可以將其改造為:

function run(gen) {
    var g = gen();

    function next(data) {
        var result = g.next(data);

        if (result.done) return;

        result.value(next);
    }

    next();
}

run(gen);

run

由此可以看到Generator 函數的自動執行需要一種機制,即當非同步操作有了結果,能夠自動交回執行權。

而兩種方法可以做到這一點。

(1)回呼函數。將非同步操作進行包裝,並暴露出回呼函數,在回呼函數裡面交回執行權。

(2)Promise 物件。將非同步操作包裝成 Promise 對象,用 then 方法交回執行權。

在兩種方法中,我們各寫了一個 run 啟動器函數,那我們能不能將這兩種方式結合在一些,寫一個通用的 run 函數呢?讓我們試試看:

// 第一版
function run(gen) {
    var gen = gen();

    function next(data) {
        var result = gen.next(data);
        if (result.done) return;

        if (isPromise(result.value)) {
            result.value.then(function(data) {
                next(data);
            });
        } else {
            result.value(next)
        }
    }

    next()
}

function isPromise(obj) {
    return 'function' == typeof obj.then;
}

module.exports = run;
其實實作的很簡單,判斷 result.value 是否是 Promise,是就加入 then 函數,不是就直接執行。

return Promise

我們已經寫了一個不錯的啟動器函數,支援 yield 後面跟著回呼函數或 Promise 物件。

現在有一個問題需要思考,就是我們要如何獲得 Generator 函數的回傳值呢?又如果 Generator 函數中出現了錯誤,就例如 fetch 了一個不存在的接口,這個錯誤該如何捕獲呢?

這很容易讓人想到 Promise,如果這個啟動器函數回傳一個 Promise,我們就可以為這個 Promise 物件加上 then 函數,當所有的非同步操作執行成功後,我們執行 onFullfilled 函數,如果有任何失敗,就執行 onRejected 函數。

我們寫一版:

// 第二版
function run(gen) {
    var gen = gen();

    return new Promise(function(resolve, reject) {

        function next(data) {
            try {
                var result = gen.next(data);
            } catch (e) {
                return reject(e);
            }

            if (result.done) {
                return resolve(result.value)
            };

            var value = toPromise(result.value);

            value.then(function(data) {
                next(data);
            }, function(e) {
                reject(e)
            });
        }

        next()
    })

}

function isPromise(obj) {
    return 'function' == typeof obj.then;
}

function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if ('function' == typeof obj) return thunkToPromise(obj);
    return obj;
}

function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
        fn(function(err, res) {
            if (err) return reject(err);
            resolve(res);
        });
    });
}

module.exports = run;
與第一版有很大的不同:

首先,我們回傳了一個Promise,當result.done 為true 的時候,我們將該值resolve(result.value),如果執行的過程中出現錯誤,被catch 住,我們會將原因reject(e)。

其次,我們會使用 thunkToPromise 將回呼函數包裝成一個 Promise,然後統一的新增 then 函數。這裡值得注意的是,在thunkToPromise 函數中,我們遵循了error first 的原則,這意味著當我們處理回呼函數的情況時:

// 模拟数据请求
function fetchData(url) {
    return function(cb) {
        setTimeout(function() {
            cb(null, { status: 200, data: url })
        }, 1000)
    }
}
在成功時,第一個參數應該會傳回null ,表示沒有錯誤原因。

優化

我們在第二版的基礎上將程式碼寫的更簡潔優雅一點,最終的程式碼如下:

// 第三版
function run(gen) {

    return new Promise(function(resolve, reject) {
        if (typeof gen == 'function') gen = gen();

        // 如果 gen 不是一个迭代器
        if (!gen || typeof gen.next !== 'function') return resolve(gen)

        onFulfilled();

        function onFulfilled(res) {
            var ret;
            try {
                ret = gen.next(res);
            } catch (e) {
                return reject(e);
            }
            next(ret);
        }

        function onRejected(err) {
            var ret;
            try {
                ret = gen.throw(err);
            } catch (e) {
                return reject(e);
            }
            next(ret);
        }

        function next(ret) {
            if (ret.done) return resolve(ret.value);
            var value = toPromise(ret.value);
            if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
            return onRejected(new TypeError('You may only yield a function, promise ' +
                'but the following object was passed: "' + String(ret.value) + '"'));
        }
    })
}

function isPromise(obj) {
    return 'function' == typeof obj.then;
}

function toPromise(obj) {
    if (isPromise(obj)) return obj;
    if ('function' == typeof obj) return thunkToPromise(obj);
    return obj;
}

function thunkToPromise(fn) {
    return new Promise(function(resolve, reject) {
        fn(function(err, res) {
            if (err) return reject(err);
            resolve(res);
        });
    });
}

module.exports = run;

co

如果我们再将这个启动器函数写的完善一些,我们就相当于写了一个 co,实际上,上面的代码确实是来自于 co……

而 co 是什么? co 是大神 TJ Holowaychuk 于 2013 年 6 月发布的一个小模块,用于 Generator 函数的自动执行。

如果直接使用 co 模块,这两种不同的例子可以简写为:

// yield 后是一个 Promise
var fetch = require('node-fetch');
var co = require('co');

function* gen() {
    var r1 = yield fetch('https://api.github.com/users/github');
    var json1 = yield r1.json();
    var r2 = yield fetch('https://api.github.com/users/github/followers');
    var json2 = yield r2.json();
    var r3 = yield fetch('https://api.github.com/users/github/repos');
    var json3 = yield r3.json();

    console.log([json1.bio, json2[0].login, json3[0].full_name].join('\n'));
}

co(gen);
// yield 后是一个回调函数
var co = require('co');

function fetchData(url) {
    return function(cb) {
        setTimeout(function() {
            cb(null, { status: 200, data: url })
        }, 1000)
    }
}

function* gen() {
    var r1 = yield fetchData('https://api.github.com/users/github');
    var r2 = yield fetchData('https://api.github.com/users/github/followers');

    console.log([r1.data, r2.data].join('\n'));
}

co(gen);

是不是特别的好用?

以上是Generator在ES6中的自動執行詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除