JavaScript生成器式异步

时间:2014-03-17 08:31:40

标签: javascript node.js asynchronous

据我所知,在JS中编写异步代码的未来风格是使用生成器而不是回调。至少,或者尤其是在V8 / Nodejs社区。是对的吗? (但这可能是有争议的,这不是我的主要问题。)

要使用生成器编写异步代码,我找到了一些库:

它们看起来都很相似,而且我不确定它们中的哪一个使用(或者甚至是重要的)。 (但是,这可能再次引起争议,这也不是我的主要问题 - 但我仍然会对任何建议感到高兴。)

(无论如何我只使用纯V8 - 如果这很重要。我没有使用Nodejs,但我在我的自定义C ++应用程序中使用纯V8。但是,我已经有一些节点式元素我的代码,包括我的自定义require()。)

现在我有一些以回调方式编写的函数X,它本身用回调参数调用其他异步函数,例如:

function X(v, callback) {
    return Y(onGotY);
    function onGotY(err, res) {
        if(err) return callback(err);
        return Z(onGotZ);
    }
    function onGotZ(err, res, resExtended) {
        if(err) return callback(err);
        return callback(null, v + res + resExtended);
    }
}

我希望将X转换为生成器,例如我想function* X(v) { ... }。这看起来怎么样?

2 个答案:

答案 0 :(得分:1)

我使用了非常简单的自己的lib,它对我的​​小型V8环境非常有效,并且因为JS callstack保持不变,所以也很容易调试。为了使其适用于Nodejs或在网络上,它需要进行一些修改。

这里的理由:

对于调试,我们并不真正需要异步代码 - 我们希望在调试器中有一个很好理解的调用堆栈,尤其是。在node-inspector调试器中。 另请注意,到目前为止,所有异步代码都是完全人为的,我们完全同步执行所有内容,通过V8 Microtasks进行模拟。 回调式异步代码在调用堆栈中已经很难理解了。 生成器式异步代码在传统调试器中完全丢失了调用堆栈信息 - 包括与节点检查器一起使用的当前Chrome Beta Developer Tools V8调试器。这就是发电机的性质(一般是协同程序)。调试器的更高版本可能会处理,但今天情况并非如此。我们甚至需要一些特殊的C ++代码来获取信息。示例代码可以在这里找到:     https://github.com/bjouhier/galaxy-stack/blob/master/src/galaxy-stack.cc

因此,如果我们想拥有有用的调试器,今天我们就不能使用生成器 - 至少不像通常那样。 我们仍然希望使用生成器样式的异步代码,因为与回调式代码相比,它使代码更具可读性。 我们引入了一个新函数async来克服这个问题。想象一下以下回调式异步代码:

function doSthX(a, b, callback) { ... }

function doSthY(c, callback) {
    doSthX(c/2, 42, function(err, res) {
        if(err) return callback(err);
        callback(null, c + res);
    })
}

现在,使用async函数和生成器样式代码的代码相同:

function* doSthX(a, b) { ... }

function* doSthY(c) {
    var res = yield async(doSthX(c/2, 42));
    return c + res;
}

我们可以提供async(iter)的两个版本:

  1. 在堆栈顶部运行迭代器iter。这将保持一个理智的调用堆栈,一切都是串行运行。
  2. 将迭代器生成到一些较低的处理程序并使其真正异步。
  3. 请注意,有一些值得注意的库可用于第二种方法:

    https://github.com/visionmedia/co http://taskjs.org/ https://github.com/creationix/gen-run https://github.com/bjouhier/galaxy

    目前,我们只是实施第一种方法 - 使调试更容易。 如果我们曾经想要两者兼而有之,我们可以引入调试标志来通过两种实现进行切换。

    global.async = async;
    function async(iter) {
        // must be an iterator
        assert(iter.next);
    
        var gotValue;
        var sendValue;
        while(true) {
            var next = iter.next(sendValue);
            gotValue = next.value;
    
            if(!next.done) {
                // We expect gotValue as a value returned from this function `async`.
                assert(gotValue.getResult);
                var res = gotValue.getResult();
                sendValue = res;
            }
    
            if(next.done) break;
        }
    
        return {
            getResult: function() {
                return gotValue;
            }
        };
    }
    
    
    // Like `async`, but wraps a callback-style function.
    global.async_call_cb = async_call_cb;
    function async_call_cb(f, thisArg /* , ... */) {
        assert(f.apply && f.call);
        var args = Array.prototype.slice.call(arguments, 2);
        return async((function*() {
            var gotCalled = false;
            var res;
    
            // This expects that the callback is run on top of the stack.
            // We will get this if we always use the wrapped enqueueMicrotask().
            // If we have to force this somehow else at some point, we could
            // call runMicrotasks() here - or some other waiter function,
            // to wait for our callback.
            args.push(callback);
            f.apply(thisArg, args);
    
            function callback(err, _res) {
                assert(!gotCalled);
                if(err) throw err;
                gotCalled = true;
                res = _res;
            }
    
            assert(gotCalled);
            return res;
        })());
    }
    
    
    
    // get the result synchronously from async
    global.sync_from_async = sync_from_async;
    function sync_from_async(s) {
        assert(s.getResult); // see async()
        return s.getResult();
    }
    
    
    // creates a node.js callback-style function from async
    global.callback_from_async = callback_from_async;
    function callback_from_async(s) {
        return function(callback) {
            var res;
            try { res = sync_from_async(s); }
            catch(err) {
                return callback(err);
            }
            return callback(null, res); 
        };
    }
    
    
    global.sync_get = sync_get;
    function sync_get(iter) {
        return sync_from_async(async(iter));
    }
    
    
    // this is like in gen-run.
    // it's supposed to run the main-function which is expected to be a generator.
    // f must be a generator
    // returns the result.
    global.run = run;
    function run(f) {
        return sync_get(f());
    }
    

答案 1 :(得分:0)

我认为通过“将X转变为生成器”意味着“将X变成可以产生的东西”,因为这就是这些库的工作方式。

如果你正在使用co(我推荐),你需要让你的函数返回thunks,这是一个只接受回调的函数。很简单:

function coX (v) {
    return function (cb) {
        X(v, cb);
    }
}

然后只是:

co(function * () {
    yield coX('v');
})();