并行开发同步/阻塞和异步/非阻塞库-api的好方法是什么? (JavaScript的)

时间:2013-12-13 02:03:53

标签: javascript asynchronous api-design

我重写了这个问题,因为旧版本显然具有误导性。 请阅读文本并确保您了解我的要求。如果 在黑暗中还有什么东西我会修改这个问题以保持清晰。 请告诉我。

我的一个项目是将库从Python移植到JavaScript。 在I / O方面,Python库完全是阻塞/同步的 等等。对于Python代码来说,这当然是完全正常的。

我计划将同步/阻止方法移植到JavaScript 。 这有几个原因,是否值得付出努力 良好不同问题。

另外我不想添加异步/非阻塞api。

将其想象为节点中的fs模块,即fs.open和。{ fs.openSync共存。

该库是纯JavaScript,将在Node和浏览器中运行。

问题是开发这两种共存API的好/最佳方法是什么。

我认为只在一个地方发生同样的事情才是好事。 因此,可以优选实现可以共享实现的某些部分的方法。 当然没有任何代价,这就是我要问的原因。

我在这里有一个方法的提议,但我打算将其发布为 可能的答案。但是,我正在等待一些认真的讨论 在我决定接受什么作为答案之前。

到目前为止的方法是:

  • 分别实现apis和 definetly 使用promises for asynchronous functions。
  • 使用类似获取api建议的东西 - 采用更综合的方法

3 个答案:

答案 0 :(得分:3)

如果你在node.js中讨论I / O,那么大多数I / O方法都有同步版本。

没有从异步性到同步性的直接转换。我可以想到两种方法:

  1. 让每个异步方法运行一个轮询循环,等待异步任务完成后再返回。
  2. 放弃模仿同步代码的想法,而是投资于更好的编码模式(例如承诺)
  3. 为了说明我会假设选项2是更好的选择。以下示例使用Q promises(随npm install q轻松安装。

    承诺背后的想法是,尽管它们是异步的,但返回对象是一个值的 promise ,就像它是一个普通函数一样。

    // Normal function
    function foo(input) {
      return "output";
    }
    
    // With promises
    function promisedFoo(input) {
      // Does stuff asynchronously
      return promise;
    }
    

    第一个函数接受输入并返回结果。第二个示例接受一个输入并立即返回一个promise,当async任务完成时,该promise将最终解析为一个值。然后,您按如下方式管理此承诺:

    var promised_value = promisedFoo(input);
    promised_value.then(function(value) {
      // Yeah, we now have a value!
    })
    .fail(function(reason) {
      // Oh nos.. something went wrong. It passed in a reason
    });
    

    使用承诺,你不再需要担心什么时候会发生。您可以轻松地链接承诺,以便事情同步发生,而无需疯狂的嵌套回调或100个命名函数。

    非常值得学习。请记住,承诺旨在使异步代码的行为与同步代码相似,即使它不是阻塞。

答案 1 :(得分:0)

我实现了一个库,可以执行我要求的ObtainJS

(是的,图书馆使用Promises但不像其他人在这里提出的那样)

重新发布Readme.md:

ObtainJS

ObtainJS是一个将异步和异步结合在一起的微框架 同步JavaScript代码。它可以帮助你不要重复自己 (DRY)如果您正在开发具有两者接口的库 阻塞/同步和非阻塞/异步执行模型。

作为 USER

使用ObtainJS实现的库的

您无需学习 很多。通常,使用ObtainJS定义的函数具有第一个参数 开关,允许您选择执行路径,然后是正常 参数:

// readFile has an obtainJS API:
function readFile(obtainAsyncExecutionSwitch, path) { /* ... */ }

同步执行

如果obtainSwitch是一个假值,readFile将同步执行 并直接返回结果。

var asyncExecution = false, result;
try {
    result = readFile(asyncExecution, './file-to-read.js');
} catch(error) {
    // handle the error
}
// do something with result

异步执行

如果obtainSwitch是一个truthy值,readFile将异步执行 并且总是返回一个承诺。

请参阅Promises at MDN

var asyncExecution = true, promise;

promise = readFile(asyncExecution, './file-to-read.js');
promise.then(
    function(result) {
        // do something with result
    },
    function(error){
        // handle the error
    }
)

// Alternatively, use the returned promise directly:

readFile(asyncExecution, './file-to-read.js')
    .then(
        function(result) {
            // do something with result
        },
        function(error){
            // handle the error
        }
    )

您也可以使用基于回调的api。请注意,无论如何都会返回Promise。

var asyncExecution;

function unifiedCallback(error, result){
    if(error)
        // handle the error
    else
        // do something with result
}

asyncExecution = {unified: unifiedCallback}

readfile(asyncExecution, './file-to-read.js');

或使用单独的回调和错误回复

var asyncExecution;

function callback(result) {
    // do something with result
}

function errback(error) {
    // handle the error
}

var asyncExecution = {callback: callback, errback: errback}
readfile(asyncExecution, './file-to-read.js');

```

智能;-) LIBRARY AUTHOR

谁将使用ObtainJS实现API,工作更多一点。 和我在一起。

通过定义双重依赖树来实现上述行为:一 用于同步执行路径的操作和用于操作的操作 异步执行路径。

动作是依赖于其他结果的小函数 动作。异步执行路径将回退到同步 如果没有为依赖项定义异步操作,则执行操作。 如果同步,则不会定义异步操作 等效的是非阻塞。这是 DRY

的地方

所以,你所做的就是分裂你的同步和阻塞 小功能中的方法。这些帆船取决于每个的结果 其他。然后为每个定义一个非阻塞的AND异步垃圾 同步和阻塞垃圾。其余的确为你获得了.JS。即:

  • 创建用于同步或异步执行的开关
  • 解析依赖关系树
  • 以正确的顺序执行游戏
  • 通过以下方式向您提供结果:
    • 使用同步路径时的返回值
    • 使用异步路径时承诺OR回调(您的选择!)

这是readFile函数

从上面直接从工作代码处获取 ufoJS

define(['ufojs/obtainJS/lib/obtain'], function(obtain) {

    // obtain.factory creates our final function
    var readFile = obtain.factory(
        // this is the synchronous dependency definition
        {
            // this action is NOT in the async tree, the async execution
            // path will fall back to this method
            uri: ['path', function _path2uri(path) {
                return path.split('/').map(encodeURIComponent).join('/')
            }]
            // synchronous AJAX request
          , readFile:['uri', function(path) {
                var request = new XMLHttpRequest();
                request.open('GET', path, false);
                request.send(null);

                if(request.status !== 200)
                    throw _errorFromRequest(request);

                return request.responseText;
            }]
        }
      ,
      // this is the asynchronous dependency definition
      {
            // aynchronous AJAX request
            readFile:['uri', '_callback', function(path, callback) {
                var request = new XMLHttpRequest()
                  , result
                  , error
                  ;
                request.open('GET', path, true);

                request.onreadystatechange = function (aEvt) {
                    if (request.readyState != 4 /*DONE*/)
                        return;

                    if (request.status !== 200)
                        error = _errorFromRequest(request);
                    else
                        result = request.responseText
                    callback(error, result)
                }
                request.send(null);
            }]
        }
      // this are the "regular" function arguments
      , ['path']
      // this is the "job", a driver function that receives as first
      // argument the obtain api. A method that the name of an action or
      // of an argument as input and returns its result
      // Note that job is potentially called multiple times during
      // asynchronoys execution
      , function(obtain, path){ return obtain('readFile'); }
    );
})

骨架

var myFunction = obtain.factory(
    // sync actions
    {},
    // async actions
    {},
    // arguments
    [],
    //job
    function(obtain){}
);

动作/获取者定义

// To define a getter we give it a name provide a definition array.
{
    // sync

    sum: ['arg1', 'arg2',
    // the last item in the definition array is always the action/getter itself.
    // it is called when all dependencies are resolved
    function(arg1, arg2) {
        // function body.
        var value = arg1 + arg2
        return value
    }]
}

// For asynchronous getters you have different options:
{
    // async

    // the special name "_callback" will inject a callback function
    sample1: ['arg1',  '_callback', function(arg1, callback) {
            // callback(error, result)
        }],
    // you can order separate callback and errback when using both special
    // names "_callback" and "_errback"
    sample2: ['arg1',  '_callback', '_errback', function(arg1, callback, errback) {
            // errback(error)
            // callback(result)
        }],
    // return a promise
    sample3: ['arg1', function(arg1) {
            var promise = new Promise(/* do what you have to*/);

            return promise
        }]
}

操作之前定义数组中的项是依赖项 他们的价值观将被注入到行动呼吁中 可用。

如果依赖项的类型不是字符串:它将作为值注入 直。通过这种方式,您可以有效地进行currying。

如果值的类型是字符串:它会在依赖项中查找 当前执行路径的树(同步或异步)。

  • 如果其名称被定义为caller-argument(在obtain.factory的第三个参数中)的值 取自调用电话。
  • 如果将其名称定义为另一个操作的名称,则该操作为 执行并将其返回值用作参数。行动会 每次运行只执行一次,以后的调用将返回缓存的值。
    • 如果执行路径是异步的,则首先查找a 异步动作定义。如果没有找到它会退回 同步定义。

如果您希望将String作为值传递给getter,则必须将其定义为 获取的实例.Argument:new obtain.Argument('mystring argument is not a getter')

更完整的例子

来自ufoLib/glifLib/GlyphSet.js

请注意:obtainJS知道主机对象并传播this 正确地对待所有行动。

    /**
     * Read the glif from I/O and cache it. Return a reference to the
     * cache object: [text, mtime, glifDocument(if alredy build by this.getGLIFDocument)]
     *
     * Has the obtainJS sync/async api.
     */
GlypSet.prototype._getGLIFcache = obtain.factory(
    { //sync
        fileName: ['glyphName', function fileName(glyphName) {
            var name = this.contents[glyphName];
            if(!(glyphName in this.contents) || this.contents[glyphName] === undefined)
                throw new KeyError(glyphName);
            return this.contents[glyphName]
        }]
      , glyphNameInCache: ['glyphName', function(glyphName) {
            return glyphName in this._glifCache;
        }]
      , path: ['fileName', function(fileName) {
            return [this.dirName, fileName].join('/');
        }]
      , mtime: ['path', 'glyphName', function(path, glyphName) {
            try {
                return this._io.getMtime(false, path);
            }
            catch(error) {
                if(error instanceof IONoEntryError)
                    error = new KeyError(glyphName, error.stack);
                throw error;
            }
        }]
      , text: ['path', 'glyphName', function(path, glyphName) {
            try {
                return this._io.readFile(false, path);
            }
            catch(error) {
                if(error instanceof IONoEntryError)
                    error = new KeyError(glyphName, error.stack);
                throw error;
            }
        }]
      , refreshedCache: ['glyphName', 'text', 'mtime',
        function(glyphName, text, mtime) {
            return (this._glifCache[glyphName] = [text, mtime]);
        }]
    }
    //async getters
  , {
        mtime: ['path', 'glyphName', '_callback',
        function(path, glyphName, callback) {
            var _callback = function(error, result){
                if(error instanceof IONoEntryError)
                    error = new KeyError(glyphName, error.stack);
                callback(error, result)
            }
            this._io.getMtime({unified: _callback}, path);
        }]
      , text: ['path', 'glyphName', '_callback',
        function(path, glyphName, callback){
            var _callback = function(error, result) {
                if(error instanceof IONoEntryError)
                    error = new KeyError(glyphName, error.stack);
                callback(error, result)
            }
            this._io.readFile({unified: _callback}, path);
        }
      ]
    }
    , ['glyphName']
    , function job(obtain, glyphName) {
        if(obtain('glyphNameInCache')) {
            if(obtain('mtime').getTime() === this._glifCache[glyphName][1].getTime()) {
                // cache is fresh
                return this._glifCache[glyphName];
            }
        }
        // still here? need read!
        // refreshing the cache:
        obtain('refreshedCache')
        return this._glifCache[glyphName];
    }
)

答案 2 :(得分:0)

使用带有async / sync标志的promises编写低级API。

更高级别的异步API直接返回这些承诺(同时也使用像1970年那样的异步回调)。

更高级别的同步API从promise同步解包值并返回值或抛出错误。

(示例使用bluebird,与Q相比,速度提高了几个数量级,并且以文件大小为代价具有更多功能,尽管这可能不适合浏览器。)

未曝光的低级别api:

//lowLevelOp calculates 1+1 and returns the result
//There is a 20% chance of throwing an error

LowLevelClass.prototype.lowLevelOp = function(async, arg1, arg2) {
    return new Promise(function(resolve, reject) {
        if (Math.random() < 0.2) {
            throw new Error("random error");
        }
        if (!async) resolve(1+1);
        else {
        //Async
            setTimeout(function(){
                resolve(1+1);
            }, 50);
        }
    });
};

使用promises或callbacks同步工作的高级公开API:

HighLevelClass.prototype.opSync = function(arg1, arg2) {
    var inspection = 
        this.lowLevel.lowLevelOp(false, arg1, arg2).inspect();

    if (inspection.isFulfilled()) {
        return inspection.value();
    }
    else {
        throw inspection.error();
    }
};

HighLevelClass.prototype.opAsync = function(arg1, arg2, callback) {
    //returns a promise as well as accepts callback.
    return this.lowLevel.lowLevelOp(true, arg1, arg2).nodeify(callback);
};

您可以自动为同步方法生成高级API:

var LowLevelProto = LowLevelClass.prototype;

Object.keys(LowLevelProto).filter(function(v) {
    return typeof LowLevelProto[v] === "function";
}).forEach(function(methodName) {
    //If perf is at all a concern you really must do this with a 
    //new Function instead of closure and reflection
    var method = function() {
        var inspection = this.lowLevel[methodName].apply(this.lowLevel, arguments);

        if (inspection.isFulfilled()) {
            return inspection.value();
        }
        else {
            throw inspection.error();
        }
    };

    HighLevelClass.prototype[methodName + "Sync" ] = method;
});