Automatic Execution of Generator in ES6 Series

Time:2019-2-21

Single asynchronous task

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

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

In order to achieve the final implementation results, you need to do this:

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

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

First, the Generator function is executed to get the traversal object.

Then the next method is used to perform the first phase of the asynchronous task, fetch (url).

Note that because fetch (url) returns a Promise object, the result value is:

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

Finally, we add a then method to the Promise object, first formatting the data it returns.(data.json()Then call g. next and pass in the data, so that the second phase of the asynchronous task can be executed and the code is executed.

Multiple Asynchronous Tasks

In the previous section, we only called one interface, so if we call more than one interface and use more than one yield, should we continue to nest in the then function?

So let’s look at executing multiple asynchronous tasks:

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

In order to get the final execution results, you may write as follows:

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

But I know you certainly don’t want to write like this…

In fact, with recursion, we can write as follows:

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

The key is to return a Promise object when yielding, add the then method to the Promise object, execute the onFullfill function when the asynchronous operation succeeds, and execute g. next in the onFullfill function, so that the Generator can continue to execute, then return a Promise, execute g. next when it succeeds, and then return…

Starter function

In the starter function run, we format the data in the then function.data.json()But in a broader context, for example, yield directly follows a Promise, rather than a Promise returned by a fetch function, because without the JSON method, the code will report an error. So in order to be more versatile, together with this example and the starter, we modify it as follows:

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

As long as the yield is followed by a Promise object, we can use this run function to automatically execute the Generator function.

callback

Do you have to follow a Promise object after yield to guarantee the automatic execution of Generator? What if it’s just a callback function? Let’s take an example:

First, let’s simulate a common asynchronous request:

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

We transform this function into:

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

For such Generator functions:

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

If the final result is to be obtained:

var g = gen();

var r1 = g.next();

r1.value(function(data) {
    var r2 = g.next(data);
    r2.value(function(data) {
        g.next(data);
    });
});

If we write this way, we will face the same problem as the first section, that is, when using multiple yields, the code will be nested circularly…

Recursion is also used, so we can transform it to:

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

From this we can see that the automatic execution of Generator function requires a mechanism, that is, when the asynchronous operation has a result, the execution right can be automatically returned.

And two ways can do that.

(1) Callback function. The asynchronous operation is wrapped, the callback function is exposed, and the execution right is returned in the callback function.

(2) Promise object. Wrap asynchronous operations into Promise objects and return execution rights with the then method.

In both ways, we write a run starter function, so can we combine these two ways to write a general run function? Let’s try:

// First Edition
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;

In fact, the implementation is very simple, judging whether result. value is Promise or not, adding the then function, or not directly executing it. ___________

return Promise

We have written a good starter function that supports yield followed by callback functions or Promise objects.

Now there’s a question to think about. How do we get the return value of the Generator function? And if there is an error in the Generator function, such as fetch, which has a non-existent interface, how can this error be caught?

It’s easy to think of Promise. If the promoter function returns a Promise, we can add the then function to the Promise object. When all the asynchronous operations are successful, we execute the onFullfilled function. If any failures occur, we execute the onRejected function.

Let’s write a edition:

// Second Edition
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;

It is very different from the first edition.

First of all, we return a Promise, when ____________result.doneWhen true, we will use this value.resolve(result.value)If there is an error in the execution process and we are caught, we will find out why.reject(e)

Secondly, we will use it.thunkToPromiseWrap the callback function as a Promise and add the then function uniformly. It is worth noting here that in the ____________thunkToPromiseIn functions, we follow the principle of error first, which means when we deal with callback functions:

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

When successful, the first parameter should return null to indicate that there is no cause for the error.

optimization

On the basis of the second edition, we write the code more concisely and elegantly. The final code is as follows:

// Third Edition
function run(gen) {

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

        // If Gen is not an iterator
        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

If we write this starter function more perfectly, we will write a co, in fact, the above code really comes from co…

And what is co? Co is a small module released by TJ Holowaychuk in June 2013 for automatic execution of Generator functions.

If you use the co module directly, these two different examples can be abbreviated as follows:

// After yield is a 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);
// After yield is a callback function
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);

Is it particularly useful?

ES6 series

ES6 Series Directory Address: https://github.com/mqyqingfeng/Blog

The ES6 series is expected to write about 20 articles, aiming at deepening the understanding of some ES6 knowledge points, focusing on block-level scopes, label templates, arrow functions, Symbol, Set, Map and Promise simulation implementation, module loading scheme, asynchronous processing and so on.

If there are any mistakes or inaccuracies, please be sure to correct them. Thank you very much. If you like it or are inspired by it, welcome star, which is also an encouragement to the author.



Author of this article: Li Yu

Read the original text

This article is the original content of Yunqi Community, which can not be reproduced without permission.

Recommended Today

Detailed explanation of sshd service and service management command under Linux

sshd SSH is the abbreviation of secure shell, which is the security protocol of application layer. SSH is a reliable protocol which provides security for remote login session and other network services. SSH protocol can effectively prevent information leakage in the process of remote management. openssh-server Function: enable remote hosts to access the sshd service […]