Home > Web Front-end > JS Tutorial > Detailed explanation of automatic execution of Generator in ES6

Detailed explanation of automatic execution of Generator in ES6

不言
Release: 2018-10-19 15:10:59
forward
2174 people have browsed it

This article brings you a detailed explanation of the automatic execution of Generator in ES6. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

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);
}
Copy after login

In order to obtain the final execution result, 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);
});
Copy after login

First execute the Generator function and obtain the traverser object .

Then use the next method to execute the first phase of the asynchronous task, which is fetch(url).

Note that since fetch(url) will return a Promise object, the value of result is:

{ value: Promise { <pending> }, done: false }
Copy after login

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

Multiple asynchronous tasks

We only called one interface in the previous section, so if we called multiple interfaces and used multiple yields, wouldn’t we have to Continuously nesting in the then function...

So let’s take a look at the execution of 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'));
}
Copy after login

In order to obtain the final execution result, you may have to write:

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)
});
Copy after login

But I know you definitely don’t want to write it like this...

In fact, using recursion, we can write like this:

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);
Copy after login

The key is to return a Promise object when yielding , add a then method to this Promise object, and execute it when the asynchronous operation succeeds The onFullfilled function in then executes g.next in the onFullfilled function, thus allowing the Generator to Continue execution, then return a Promise, execute g.next on success, and then return...

Starter function

Run this starter function , we format the data in the then function data.json(), but in more general cases, for example, yield is directly followed by a Promise, rather than a Promise returned by the fetch function, because there is no json method, the code will report an error . So in order to be more versatile, together with this example and starter, we modified it to:

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);
Copy after login

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

Callback function

Does yield must be followed by a Promise object to ensure automatic execution of the Generator? What if it's just a callback function? Let’s look at an example:

First we simulate a normal asynchronous request:

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

We transform this function into:

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

For such a Generator function:

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

If you want to get the final result:

var g = gen();

var r1 = g.next();

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

If written like this, we will face the same problem as in the first section, that is, when using multiple yields, the code will be embedded in a loop Put it together...

also uses recursion, so we can transform it into:

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

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

        if (result.done) return;

        result.value(next);
    }

    next();
}

run(gen);
Copy after login

run

From this we can see the Generator function Automatic execution requires a mechanism that can automatically hand over execution rights when the asynchronous operation has results.

And there are two ways to do this.

(1) Callback function. Wrap the asynchronous operation, expose the callback function, and return the execution rights in the callback function.

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

In each of the two methods, we wrote a run launcher function. Can we combine these two methods and write a general run function? Let’s try it:

// 第一版
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;
Copy after login

In fact, the implementation is very simple. Determine whether result.value is a Promise. If so, add the then function. If not, execute it directly.

return Promise

We have written a nice starter function that supports yield followed by a callback function or Promise object.

Now there is a question to think about, that is, how do we get the return value of the Generator function? And if an error occurs in the Generator function, such as fetching a non-existent interface, how to catch this error?

This is easy to think of Promise, if this starter function returns a Promise, we can add then to this Promise object function, when all asynchronous operations are executed successfully, we execute the onFullfilled function, and if there is any failure, we execute the onRejected function.

We write a version:

// 第二版
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;
Copy after login

It is very different from the first version:

First, we return a Promise, when result.done is true , we will resolve(result.value) the value. If an error occurs during execution and is caught, we will reject(e) the reason.

Secondly, we will use thunkToPromise to wrap the callback function into a Promise, and then add the then function uniformly. It is worth noting here that in the thunkToPromise function, we follow the error first principle, which means when we deal with the callback function situation:

// 模拟数据请求
function fetchData(url) {
    return function(cb) {
        setTimeout(function() {
            cb(null, { status: 200, data: url })
        }, 1000)
    }
}
Copy after login

On success, the first parameter should return null , indicating that there is no error cause.

Optimization

Based on the second version, we wrote the code more concisely and elegantly. The final code is as follows:

// 第三版
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;
Copy after login

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);
Copy after login
// 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);
Copy after login

是不是特别的好用?

The above is the detailed content of Detailed explanation of automatic execution of Generator in ES6. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:segmentfault.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template