Javascript异步函数组合

Javascript async function composition

本文关键字:组合 函数 异步 Javascript      更新时间:2023-09-26

我有几个具有不同参数数量的异步函数,每个参数的最后一个参数都是回调。我想按顺序称呼这些。例如。

function getData(url, callback){
}
function parseData(data, callback){
}

通过使用这个:

Function.prototype.then = function(f){ 
  var ff = this; 
  return function(){ ff.apply(null, [].slice.call(arguments).concat(f)) } 
}

可以像这样调用这些函数,并将输出打印到 Console.log。

getData.then(parseData.then(console.log.bind(console)))('/mydata.json');

我一直在尝试使用此语法,但无法正确获取 Then 函数。有什么想法吗?

getData.then(parseData).then(console.log.bind(console))('/mydata.json');

实现一个允许您像上面这样链接方法的函数或库是一项不平凡的任务,需要大量的努力。上面示例的主要问题是不断的上下文变化 - 在没有内存泄漏的情况下管理调用链的状态非常困难(即将对所有链接函数的引用保存到模块级变量中 -> GC 永远不会从内存中释放函数)。

如果你对这种编程策略感兴趣,我强烈建议你使用现有的、已建立的、经过良好测试的库,比如 Promise 或 q。我个人推荐前者,因为它试图尽可能接近 ECMAScript 6 的 Promise 规范。

出于教育目的,我建议您查看 Promise 库的内部工作原理 - 我很确定通过检查其源代码并使用它,您会学到很多东西。

罗伯特·

罗斯曼是对的。但我愿意纯粹出于学术目的回答。

让我们将您的代码简化为:

Function.prototype.then = function (callback){ 
  var inner = this;
  return function (arg) { return inner(arg, callback); }
}

和:

function getData(url, callback) {
    ...
}

让我们分析一下每个函数的类型:

  • getData (string, function(argument, ...)) → null.
  • function(argument, function).then (function(argument, ...)) → function(argument).

这是问题的核心。当您执行以下操作时:

getData.then(function (argument) {})它实际上返回了一个类型为 function(argument) 的函数。这就是为什么不能调用.then的原因,因为.then希望被调用到function(argument, function)类型上。

您要做的是包装回调函数。(在getData.then(parseData).then(f)的情况下,你想用f包裹parseData,而不是getData.then(parseData)的结果。

这是我的解决方案:

Function.prototype.setCallback = function (c) { this.callback = c; }
Function.prototype.getCallback = function () { return this.callback; }
Function.prototype.then = function (f) {
  var ff = this;
  var outer = function () {
     var callback = outer.getCallback();
     return ff.apply(null, [].slice.call(arguments).concat(callback));
  };
  if (this.getCallback() === undefined) {
    outer.setCallback(f);
  } else {
    outer.setCallback(ff.getCallback().then(f));
  }
  return outer;
}

这看起来是 Promise 对象的绝佳用法。Promise 通过为异步计算提供通用接口来提高回调函数的可重用性。Promise 允许您将函数的异步部分封装在 Promise 对象中,而不是让每个函数接受回调参数。然后,您可以使用 Promise 方法(Promise.all、Promise.prototype.then)将异步操作链接在一起。下面是您的示例的翻译方式:

// Instead of accepting both a url and a callback, you accept just a url. Rather than
// thinking about a Promise as a function that returns data, you can think of it as
// data that hasn't loaded or doesn't exist yet (i.e., promised data).
function getData(url) {
    return new Promise(function (resolve, reject) {
        // Use resolve as the callback parameter.
    });
}
function parseData(data) {
    // Does parseData really need to be asynchronous? If not leave out the
    // Promise and write this function synchronously.
    return new Promise(function (resolve, reject) {
    });
}
getData("someurl").then(parseData).then(function (data) {
    console.log(data);
});
// or with a synchronous parseData
getData("someurl").then(function (data) {
    console.log(parseData(data));
});

另外,我应该指出,Promises目前没有出色的浏览器支持。幸运的是,您被覆盖了,因为有很多像这样的 polyfill 提供了与本机 Promise 相同的功能。

编辑:

或者,与其更改 Function.prototype,不如实现一个链方法,该方法将异步函数列表和种子值作为输入,并通过每个异步函数管道传输该种子值:

function chainAsync(seed, functions, callback) {
    if (functions.length === 0) callback(seed);
    functions[0](seed, function (value) {
        chainAsync(value, functions.slice(1), callback);
    });
}
chainAsync("someurl", [getData, parseData], function (data) {
    console.log(data);
});

再次编辑:

上面介绍的解决方案远非强大,如果您想要更广泛的解决方案,请查看类似 https://github.com/caolan/async 的内容。

我对这个问题有一些想法,并创建了以下代码,它有点满足您的要求。仍然 - 我知道这个概念远非完美。原因在代码和下面进行了注释。

Function.prototype._thenify = {
    queue:[],
    then:function(nextOne){
        // Push the item to the queue
        this._thenify.queue.push(nextOne);
        return this;
    },
    handOver:function(){
        // hand over the data to the next function, calling it in the same context (so we dont loose the queue)
        this._thenify.queue.shift().apply(this, arguments);
        return this;
    }
}
Function.prototype.then = function(){ return this._thenify.then.apply(this, arguments) };
Function.prototype.handOver = function(){ return this._thenify.handOver.apply(this, arguments) };
function getData(json){
    // simulate asyncronous call
    setTimeout(function(){ getData.handOver(json, 'params from getData'); }, 10);
    // we cant call this.handOver() because a new context is created for every function-call
    // That means you have to do it like this or bind the context of from getData to the function itself
    // which means every time the function is called you have the same context
}
function parseData(){
    // simulate asyncronous call
    setTimeout(function(){ parseData.handOver('params from parseData'); }, 10);
    // Here we can use this.handOver cause parseData is called in the context of getData
    // for clarity-reasons I let it like that
}
getData
    .then(function(){ console.log(arguments); this.handOver(); }) // see how we can use this here
    .then(parseData)
    .then(console.log)('/mydata.json');                           // Here we actually starting the chain with the call of the function
    
// To call the chain in the getData-context (so you can always do this.handOver()) do it like that:
// getData
//     .then(function(){ console.log(arguments); this.handOver(); })
//     .then(parseData)
//     .then(console.log).bind(getData)('/mydata.json');

问题和事实:

  • 完整链在第一个函数的上下文中执行
  • 你必须使用函数本身来调用 handOver,至少使用链的第一个元素
  • 如果您使用已经使用的功能创建新链,则当它运行到同一时间时,它将发生冲突
  • 可以在链中使用一个函数两次(例如getData)
  • 由于共享的 CONEXT,您可以在一个函数中设置属性并在以下函数之一中读取它

至少对于第一个问题,您可以通过不在同一上下文中调用链中的下一个函数而是将队列作为参数提供给下一个函数来解决它。我稍后会尝试这种方法。这也许也可以解决第3点提到的冲突。

对于另一个问题,您可以在注释中使用示例代码

PS:当您运行截图时,请确保您的控制台已打开以查看输出

PPS:欢迎对这种方法发表任何评论!

问题是then返回当前函数的包装器,连续的链接调用将再次包装它,而不是包装上一个回调。实现此目的的一种方法是在每个调用上使用闭包并覆盖then

Function.prototype.then = function(f){ 
  var ff = this;
  function wrapCallback(previousCallback, callback) {
    var wrapper = function(){ 
      previousCallback.apply(null, [].slice.call(arguments).concat(callback)); 
    };
    ff.then = wrapper.then = function(f) {
      callback = wrapCallback(callback, f); //a new chained call, so wrap the callback
      return ff;    
    }
    return wrapper;
  }
  
  return ff = wrapCallback(this, f); //"replace" the original function with the wrapper and return that
}
/*
 * Example
 */ 
function getData(json, callback){
    setTimeout( function() { callback(json) }, 100);
}
function parseData(data, callback){
   callback(data, 'Hello');
}
function doSomething(data, text, callback) {
  callback(text);  
}
function printData(data) {
  console.log(data); //should print 'Hello'
}
getData
    .then(parseData)
    .then(doSomething)
    .then(printData)('/mydata.json');