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);
}

为了得到最终的执行结果,你须要这样作:node

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

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

首先执行 Generator 函数,获取遍历器对象。git

而后使用 next 方法,执行异步任务的第一阶段,即 fetch(url)。github

注意,因为 fetch(url) 会返回一个 Promise 对象,因此 result 的值为:json

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

最后咱们为这个 Promise 对象添加一个 then 方法,先将其返回的数据格式化(data.json()),再调用 g.next,将得到的数据传进去,由此能够执行异步任务的第二阶段,代码执行完毕。api

多个异步任务

上节咱们只调用了一个接口,那若是咱们调用了多个接口,使用了多个 yield,咱们岂不是要在 then 函数中不断的嵌套下去……promise

因此咱们来看看执行多个异步任务的状况:异步

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)
});

但我知道你确定不想写成这样……fetch

其实,利用递归,咱们能够这样写:优化

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);

是否是特别的好用?

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级做用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。

相关文章
相关标签/搜索