JavaScript异步函数组合

11

我有几个异步函数,它们的参数数量各不相同,在每个函数中,最后一个参数是回调函数。我希望按顺序调用它们。例如:

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');

2
getData.then() 不会涉及调用函数 getData()。我认为你的方法有误。 - Pointy
7
我可以,你为什么不使用Promise库(例如q)? https://github.com/kriskowal/q - Antiga
1
FYI,像Bluebird这样的库中的.promisify()方法将为您完成所有这些工作,然后您可以执行类似于getDataAsync(...).then(parseDataAsync)的操作。如果您想自己实现该功能而不使用第三方库,则可以查看Bluebird中的实现方式并从中学习。 - jfriend00
原则上,您必须用自己的回调函数替换回调函数,当调用您自己的回调函数时,您可以调用原始回调函数,然后调用链中的下一个项目。 您当前的代码就是没有做到这一点。 - jfriend00
问题在于 parseData 无法从 getData 中获取结果。 - hjl
显示剩余5条评论
5个回答

13
实现一个允许您像上面那样链接方法的函数或库是一项非常复杂的任务,需要大量的努力。上面例子的主要问题是不断变化的上下文环境 - 很难在没有内存泄漏的情况下管理调用链的状态(即将所有链接函数的引用保存到模块级变量中 -> GC永远不会从内存中释放这些函数)。
如果您对这种编程策略感兴趣,我强烈建议您使用现有的、经过验证和测试的库,例如Promiseq。个人推荐前者,因为它试图尽可能接近ECMAScript 6的Promise规范。
出于教育目的,我建议您查看Promise库的内部工作方式 - 我相信通过检查其源代码并进行调试,您将学到很多。

3

Robert Rossmann是正确的。但我愿意出于学术目的回答。

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

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;
}

3

这看起来是Promise对象的一个很好的应用。 Promises通过提供异步计算的公共接口来提高回调函数的可重用性。不必让每个函数都接受一个回调参数,Promises允许您将函数的异步部分封装在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));
});

此外,我应该指出,Promise 目前在浏览器中的支持不太好。幸运的是,有很多 polyfill,比如this one,它们提供了与原生 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 的内容。


我有些假设OP不愿意重构现有函数的代码,但我没有在任何地方看到这种说法。好答案,展示了Promise库的实际用法。 - Robert Rossmann

2

我就那个问题有一些想法,写了下面的代码,勉强符合你的要求。但是我知道这种概念还很不完美。原因在代码中已经注释,下面也会提到。

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)。 - 由于共享上下文,您可以在一个函数中设置属性,并在后续函数中读取它。
至少对于第一个问题,您可以通过不在相同上下文中调用链中的下一个函数,而是将队列作为参数传递给下一个函数来解决它。我稍后会尝试这种方法。这也可能会解决在第3点提到的冲突。
对于其他问题,您可以使用注释中的示例代码。
PS:运行片段时,请确保您的控制台已打开以查看输出。
PPS:欢迎对此方法发表任何评论!

应该在每个函数实例上创建 _thenify。否则,您将操纵在所有函数实例之间共享的队列。 - Ginden
是的 - 我在一个链中共享队列,以便链中的每个函数都可以访问它。问题在于每个函数都返回第一个函数的上下文,以确保我们最后调用第一个函数。 - Fuzzyma

2
问题在于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');


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接