thunk関数とcoフレームを徹底的に理解する

19450 ワード

ES 6は多くの新しい特性を持ってきました.その中で、生成器、yieldなどは前のピラミッド式の非同期フィードバックに対してよく解決できます.このパッケージに基づくcoフレームは完全に同期した方式で非同期コードを作成することができます.この文章はジェネレータ関数とフレームthunkify、coのコアコードを比較的に徹底的に分析します.coはまだ広く使われています.私たちの日常的なコードのほかに、いくつかの有名なフレームもcoに基づいて実現されています.例えば、次世代のNodejs webフレームと呼ばれるkoaなどです.
ジェネレータ関数
ジェネレータ関数は、書き込みです.
function* func(){}
書式コードは、その本質も関数であるため、一般関数が持つすべての特性を備えています.これ以外にも、以下のような有用な特性がある.
  • は、ジェネレータ関数を実行して一つのジェネレータに戻り、ジェネレータはthrow()の方法を持ち、手動で異常を投げてもよく、ジェネレータかどうかを判断するのに用いられます.
  • は、ジェネレータ関数の内部でyield(またはyield*)を使用し、関数がyieldに実行されると実行を一時停止し、yieldの右値(関数コンテキスト、変数のバインディングなどの情報が保持されます)を返します.ジェネレータのnext()方法により、現在のyieldの右式の値(value属性)を含むオブジェクトを返します.およびgenerator関数が既に実行済みであるかどうかなどの情報.next()メソッドを実行するたびに、前回実行したyieldのところから下に降りて、次のyieldに会って、関連実行情報を含む対象に戻ってから一時停止して、次のnext()の実行を待つ.
  • 生成器のnext()メソッドは、ieldの右式の値と実行済みの情報を含むオブジェクトを返します.next()メソッドのパラメータは前の一時停止でyieldの戻り値です.
  • 以下は例で説明します.
    例1:
    function test(){
        return 'b';
    }
    
    function* func(){
        
    var a = yield 'a';
    consolone.log('gen:',a)/.gen:undefined var b=yield test()consolone.log('gen:',b)/.gen:undefined
    }var func 1=func()var a=func 1.next();consolie.log('next:',a)/.next:{value:'a',done:false}var b=func 1.next();consolie.log('next:',b)/.next:{value:'b',done:false}var c=func 1.next();consolie.log('next:',c)//.next:{value:undefined,done:true}
             3     :“    next()        yield                  ; next()            yield    ”,           next()      ,  :var a = yield 'a'; a   undefined。 
      

    2:

    function test(){
        return 'b';
    }
    
    function* func(){
        var a = yield 'a';
        console.log('gen:',a);// gen:1
        var b = yield test();
        console.log('gen:',b);// gen:2
    }
    var func2 = func();
    var a = func2.next();
    console.log('next:', a);// next: { value: 'a', done: false }
    var b = func2.next(1);
    console.log('next:', b);// next: { value: 'b', done: false }
    var c = func2.next(2);
    console.log('next:', c);// next: { value: undefined, done: true }
    これは にはっきりしています.これ しません.
    yieldについて*
    yieldは を し、 のみを します. 、yield*は、 を のジェネレータまたは なオブジェクトに します. の を します
    アーグメンント
    function* genFunc(){
        yield arguments;
        yield* arguments;
    }
    
    var gen = genFunc(1,2);
    console.log(gen.next().value); // { '0': 1, '1': 2 }
    console.log(gen.next().value); // 1
    console.log(gen.next().value); // 2
    Generator
    function* gen1(){
        yield 2;
        yield 3;
    }
    
    function* gen2(){
        yield 1;
        yield* gen1();
        yield 4;
    }
    
    var g2 = gen2();
    console.log(g2.next().value); // 1
    console.log(g2.next().value); // 2
    console.log(g2.next().value); // 3
    console.log(g2.next().value); // 4
    thunk
    coのアプリケーションでは、 コードを くように コードを くために、thunk を することが いです.ファイルの を み るステップ fs.readFile()のような で、thunk に する は の りです.
    function readFile(path, encoding){
        return function(cb){
            fs.readFile(path, encoding, cb);
        };
    }
    では、thunk とは ですか?
    thunk は の2つの を えています.
  • があり、 つのパラメータだけがcalbackの です.
  • calbackの のパラメータはerrorです.
  • thunk を って、 にcoを して、 コードを くように、 コードを くことができます.
    var co = require('co'),
        fs = require('fs'),
        Promise = require('es6-promise').Promise;
    
    function readFile(path, encoding){
        return function(cb){
            fs.readFile(path, encoding, cb);
        };
    }
    
    
    co(function* (){//      ,  co          promise.then().then()..       
        var a = yield readFile('a.txt', {encoding: 'utf8'});
        console.log(a); // a
        var b = yield readFile('b.txt', {encoding: 'utf8'});
        console.log(b); // b
        var c = yield readFile('c.txt', {encoding: 'utf8'});
        console.log(c); // c
        return yield Promise.resolve(a+b+c);
    }).then(function(val){
        console.log(val); // abc
    }).catch(function(error){
        console.log(error);
    });
    かっこいいですか? にかっこいいです
    は、 でthunk を くのは くさいです.thunkifyは に できるフレームがあります. のコードは の りです.
    var co = require('co'),
        thunkify = require('thunkify'),
        fs = require('fs'),
        Promise = require('es6-promise').Promise;
    
    var readFile = thunkify(fs.readFile);
    
    
    co(function* (){//      ,  co          promise.then().then()..       
        var a = yield readFile('a.txt', {encoding: 'utf8'});
        console.log(a); // a
        var b = yield readFile('b.txt', {encoding: 'utf8'});
        console.log(b); // b
        var c = yield readFile('c.txt', {encoding: 'utf8'});
        console.log(c); // c
        return yield Promise.resolve(a+b+c);
    }).then(function(val){
        console.log(val); // abc
    }).catch(function(error){
        console.log(error);
    });
    thunkifyの について、 まかな は の りである.
    /**
     * Module dependencies.
     */
    
    var assert = require('assert');
    
    /**
     * Expose `thunkify()`.
     */
    
    module.exports = thunkify;
    
    /**
     * Wrap a regular callback `fn` as a thunk.
     *
     * @param {Function} fn
     * @return {Function}
     * @api public
     */
    
    function thunkify(fn) {
        assert('function' == typeof fn, 'function required');
        //       thunk     ,   thunk      yield,          thunk      
        return function() {
            var args = new Array(arguments.length);
            //          , fn      
            var ctx = this;
    
            //            (        ,    Array.prototype.slice.call(arguments)  )
            for (var i = 0; i < args.length; ++i) {
                args[i] = arguments[i];
            }
    
            //    thunk  (         callback   , callback       error)
            //    :
            // function(cb) {fs.readFile(path, {encoding: 'utf8}, cb)}
            return function(done) {
                var called;
    
                //           ,      ;  ,            push     
                args.push(function() {
                    if (called) return;
                    called = true;
                    done.apply(null, arguments);
                });
    
                try {
                    //  ctx     fn(       , :fs.readFile)
                    //     thunkify          ( done  )  ,     :
                    // fs.readFile(path, {encoding: 'utf8}, done)
                    //   done     ,   co  
                    fn.apply(ctx, args);
                } catch (err) {
                    done(err);
                }
            }
        }
    };
    コードは ではないので、 を れば かります.
    coフレーム
    たちは のフレームを に べます.
    /**
     * slice() reference.
     */
    
    var slice = Array.prototype.slice;
    
    /**
     * Expose `co`.
     */
    
    module.exports = co['default'] = co.co = co;
    
    /**
     * Wrap the given generator `fn` into a
     * function that returns a promise.
     * This is a separate function so that
     * every `co()` call doesn't create a new,
     * unnecessary closure.
     *
     * @param {GeneratorFunction} fn
     * @return {Function}
     * @api public
     */
    
    co.wrap = function(fn) {
        createPromise.__generatorFunction__ = fn;
        return createPromise;
    
        function createPromise() {
            return co.call(this, fn.apply(this, arguments));
        }
    };
    
    /**
     * Execute the generator function or a generator
     * and return a promise.
     *
     * @param {Function} fn
     * @return {Promise}
     * @api public
     */
    // gen          (            )        (generator      )
    function co(gen) {
        //        
        var ctx = this;
        //  gen       
        var args = slice.call(arguments, 1)
    
        // we wrap everything in a promise to avoid promise chaining,
        // which leads to memory leak errors.
        // see https://github.com/tj/co/issues/180
        //     Promise  ,             co:
        /**
         * co(function*(){}).then(function(val){
         *
         * });
         * */
        return new Promise(function(resolve, reject) {
            //   gen                
            if (typeof gen === 'function') {
                gen = gen.apply(ctx, args);
            }
            //   gen     ,     
            if (!gen || typeof gen.next !== 'function') {
                return resolve(gen);
            }
    
            //     ,  generator   
            onFulfilled();
    
            /**
             * @param {Mixed} res
             * @return {Promise}
             * @api private
             */
    
            // res    :   yield     value  ({done:false,value:''} value  )
            // ret    :  yield    (  {done:false,value:''})
            // generator  :      next()     ,    yield        ,
            // next()      yield      (done) yield        (value),
            //  next()         :var a=yield cb();a  ,     
    
            /**
             *   :co(function*(){
             *     var a = yield readFile('a.txt');
             *     console.log(a);
             *     var b = yield readFile('b.txt);
             *     console.log(b);
             * });
             *       generator   ,res  a,b  
             * */
            function onFulfilled(res) {
                var ret;
                try {
                    //     co yield       。  co yield  thunk   ret.value  thunk  
                    ret = gen.next(res);
                } catch (e) {
                    return reject(e);
                }
                next(ret);
            }
    
            /**
             * @param {Error} err
             * @return {Promise}
             * @api private
             */
    
            function onRejected(err) {
                var ret;
                try {
                    ret = gen.throw(err);
                } catch (e) {
                    return reject(e);
                }
                next(ret);
            }
    
            /**
             * Get the next value in the generator,
             * return a promise.
             *
             * @param {Object} ret
             * @return {Promise}
             * @api private
             */
    
            function next(ret) {
                //       ,        :
                /**
                 * co(function*(){
                 *      return yield Promise.resolve(1);
                 * }).then(function(val){
                 *      console.log(val); // 1
                 * });
                 * */
                //  ret.value       then     val  
                if (ret.done) {
                    return resolve(ret.value);
                }
                //        ret.value   Promise  ,     :
                // promise.then(onFulfilled).then(onFulfilled).then(onFulfilled)...
                var value = toPromise.call(ctx, ret.value);
                if (value && isPromise(value)) {
                    //   onFulfilled           yield     value 
                    return value.then(onFulfilled, onRejected);
                }
                return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"'));
            }
        });
    }
    
    /**
     * Convert a `yield`ed value into a promise.
     *
     * @param {Mixed} obj
     * @return {Promise}
     * @api private
     */
    
    function toPromise(obj) {
        if (!obj) return obj;
        if (isPromise(obj)) return obj;
        if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
        if ('function' == typeof obj) return thunkToPromise.call(this, obj);
        if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
        if (isObject(obj)) return objectToPromise.call(this, obj);
        return obj;
    }
    
    /**
     * Convert a thunk to a promise.
     *
     * @param {Function}
     * @return {Promise}
     * @api private
     */
    
    function thunkToPromise(fn) {
        var ctx = this;
        return new Promise(function(resolve, reject) {
            fn.call(ctx, function(err, res) {
                if (err) return reject(err);
                if (arguments.length > 2) res = slice.call(arguments, 1);
                resolve(res);
            });
        });
    }
    
    /**
     * Convert an array of "yieldables" to a promise.
     * Uses `Promise.all()` internally.
     *
     * @param {Array} obj
     * @return {Promise}
     * @api private
     */
    
    function arrayToPromise(obj) {
        return Promise.all(obj.map(toPromise, this));
    }
    
    /**
     * Convert an object of "yieldables" to a promise.
     * Uses `Promise.all()` internally.
     *
     * @param {Object} obj
     * @return {Promise}
     * @api private
     */
    
    function objectToPromise(obj) {
        var results = new obj.constructor();
        var keys = Object.keys(obj);
        var promises = [];
        for (var i = 0; i < keys.length; i++) {
            var key = keys[i];
            var promise = toPromise.call(this, obj[key]);
            if (promise && isPromise(promise)) defer(promise, key);
            else results[key] = obj[key];
        }
        return Promise.all(promises).then(function() {
            return results;
        });
    
        function defer(promise, key) {
            // predefine the key in the result
            results[key] = undefined;
            promises.push(promise.then(function(res) {
                results[key] = res;
            }));
        }
    }
    
    /**
     * Check if `obj` is a promise.
     *
     * @param {Object} obj
     * @return {Boolean}
     * @api private
     */
    
    function isPromise(obj) {
        return 'function' == typeof obj.then;
    }
    
    /**
     * Check if `obj` is a generator.
     *
     * @param {Mixed} obj
     * @return {Boolean}
     * @api private
     */
    
    function isGenerator(obj) {
        return 'function' == typeof obj.next && 'function' == typeof obj.throw;
    }
    
    /**
     * Check if `obj` is a generator function.
     *
     * @param {Mixed} obj
     * @return {Boolean}
     * @api private
     */
    function isGeneratorFunction(obj) {
        var constructor = obj.constructor;
        if (!constructor) return false;
        if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
        return isGenerator(constructor.prototype);
    }
    
    /**
     * Check for plain object.
     *
     * @param {Mixed} val
     * @return {Boolean}
     * @api private
     */
    
    function isObject(val) {
        return Object == val.constructor;
    }
    についてコメントします. 、 たちは の に づいて、coの プロセスを します.
    たちの は:
    var co = require('co'),
        thunkify = require('thunkify'),
        fs = require('fs'),
        Promise = require('es6-promise').Promise;
    
    function readFile(path, encoding){
        return function(cb){
            fs.readFile(path, encoding, cb);
        };
    }
    
    //var readFile = thunkify(fs.readFile);
    
    
    co(function* (){//      ,  co          promise.then().then()..       
        var a = yield readFile('a.txt', {encoding: 'utf8'});
        console.log(a); // a
        var b = yield readFile('b.txt', {encoding: 'utf8'});
        console.log(b); // b
        var c = yield readFile('c.txt', {encoding: 'utf8'});
        console.log(c); // c
        return yield Promise.resolve(a+b+c);
    }).then(function(val){
        console.log(val); // abc
    }).catch(function(error){
        console.log(error);
    });
    まず、co() を します. は コンテキスト を しています.
    //        
        var ctx = this;
        //  gen       
        var args = slice.call(arguments, 1)
    
        // we wrap everything in a promise to avoid promise chaining,
        // which leads to memory leak errors.
        // see https://github.com/tj/co/issues/180
        //     Promise  ,             co:
        /**
         * co(function*(){}).then(function(val){
         *
         * });
         * */
        return new Promise(function(resolve, reject) {
        });
    たちは にこのPromiseの を て をしましたか?
    if (typeof gen === 'function') {
        gen = gen.apply(ctx, args);
    }
    は、まず、co() の のパラメータが かどうかを し、そうであれば、gen のパラメータをこの に して、genに る.ここではgenはジェネレータ ですので、ジェネレータを します.
    if (!gen || typeof gen.next !== 'function') {
         return resolve(gen);
    }
    の で、もしgenがこの ジェネレータではないならば、 Promiseのresoveを して、 はgenをco().then(function(val){}に えます.のvalになりました
    たちのこの はgenがジェネレータであり、 して します.
    onFulfilled();
    その 、 たちはcoのコア :OnFulfilledに いました. たちはこの が をしたかを ます.
    function onFulfilled(res) {
        var ret;
        try {
            ret = gen.next(res);
        } catch (e) {
            return reject(e);
        }
        next(ret);
    }
    りを ぐために、 のエラーの はしばらく します.
    この を めて します.レスポンス はundefinedです.そして、ジェネレータのnext()メソッドを します. の では します.
    var a = yield readFile('a.txt', {encoding: 'utf8'});
    では、retは です.
    {
        done: false,
        value: function(cb){
            fs.readFile(path, encoding, cb);
        }
    }
    retをnext に えます.next は:
    function next(ret) {
                if (ret.done) {
                    return resolve(ret.value);
                }
               
                var value = toPromise.call(ctx, ret.value);
                if (value && isPromise(value)) {
                    return value.then(onFulfilled, onRejected);
                }
                return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"'));
            }
    まず、ジェネレータ で されたかどうかを し、 したら のresoveが されます. らかに の では のyieldまで しましたが、 が わっていません. が していない 、ret.valueをPromiseの に します.ここはthunk です.だから、toPromiseは に します.
    function toPromise(obj) {
        if (!obj) return obj;
        if (isPromise(obj)) return obj;
        if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
        if ('function' == typeof obj) return thunkToPromise.call(this, obj);
        if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
        if (isObject(obj)) return objectToPromise.call(this, obj);
        return obj;
    }
    
    /**
     * Convert a thunk to a promise.
     *
     * @param {Function}
     * @return {Promise}
     * @api private
     */
    
    function thunkToPromise(fn) {
        var ctx = this;
        return new Promise(function(resolve, reject) {
            fn.call(ctx, function(err, res) {
                if (err) return reject(err);
                if (arguments.length > 2) res = slice.call(arguments, 1);
                resolve(res);
            });
        });
    }
    は、プロミスのインスタンスに ります.この で、fnも しました.fnは:function(cb){}で、ここに して、function{}がfnに えられたcbです. のパラメータはerrorオブジェクトです. つ のパラメータresはファイルを んだ のデータです.そして、resoveを して、 を のthen の に えます.ここで します.
    if (value && isPromise(value)) {
        return value.then(onFulfilled, onRejected);
    }
    は、つまりオンフルFilledのパラメータresである. の の によれば、resはジェネレータのnext()メソッドに えられたものであり、つまり、co ジェネレータの パラメータのvar=yield readFile('a.txt',encoding:'utf 8');のaの は と た になることができます.
    このように、thunk に づくcoフレーム のプログラミングは、 のPromise、Generator、Generator Function、Object、Arayモードの があり、これ の は われない.
    coの ロジックを して、 い をよりよく できます. はkoaなどのcoベースの みを うことによって、 たちももっと く になります.
    coの は、coの ロジックをより く するために、インターネット でもう つの の があります.
    function co(generator) {
      return function(fn) {
    	var gen = generator();
    	function next(err, result) {
    		if(err){
    			return fn(err);
    		}
    		var step = gen.next(result);
    		if (!step.done) {
    			step.value(next);
    		} else {
    			fn(null, step.value);
    		}
    	}
    	next();
       }
    }
    しかし、この は、yieldの ろだけがthunk である をサポートします. :
    var co = require('./co');
    // wrap the function to thunk
    function readFile(filename) {//     ,yield          thunk  
        return function(callback) {
    	    require('fs').readFile(filename, 'utf8', callback);
        };
    }
    
    co(function * () {
        var file1 = yield readFile('./file/a.txt');
        var file2 = yield readFile('./file/b.txt');
    
        console.log(file1);
        console.log(file2);
        return 'done';
    })(function(err, result) {
        console.log(result)
    });
    されます
    content in a.txt
    content in b.txt
    done