【问题标题】:Javascript - too much recursion when executing long chain of promisesJavascript - 执行长链承诺时递归过多
【发布时间】:2013-12-14 13:31:53
【问题描述】:

我有一长串要按顺序执行的异步函数。所以我使用了 Promise 来创建 Promise 队列。不幸的是,在 Linux 上的 Firefox 25.0.1 上,我收到 too much recursion JavaScript 错误。它在 Mac 上的相同 Firefox 版本上运行良好,它也适用于 chrome。但我需要它在任何地方都能工作,包括 Linux。你能建议更好地实现executePromiseQueueSync函数吗?

//-----------------------------------------------------------------------------------------

function executePromiseQueueSync(queue){
     var seed = $.Deferred(); 
     var acc = seed;

     for (var i = 0; i < queue.length; ++i )
     {
         var promise = queue[i];
         acc = acc.then(promise.funct.apply(null, promise.argmnt));
     }

    seed.resolve();
    return acc;
}

//-----------------------------------------------------------------------------------------

function someTask(){
    var dfd = new jQuery.Deferred();
    dfd.notify();
    dfd.resolve();
    return dfd.promise();
}

//-----------------------------------------------------------------------------------------

$(function(){
    var promisesQueue = []

    for(var i = 0; i < 200; ++i){
        promisesQueue.push({funct:someTask, argmnt:[]});
    }

    executePromiseQueueSync(promisesQueue).then(function(){alert('done!');});
});

//-----------------------------------------------------------------------------------------

JSFiddle 示例:http://jsfiddle.net/C2YN4/4/

我目前的情况(我非常愿意接受其他建议和更正):

function executePromiseQueueSync(queue){

    if (queue.length > TRESHOLD){
        var i,j;
        var shortQueue = []
        for (i=0,j=queue.length; i<j; i+=TRESHOLD) {
          temparray = queue.slice(i, i+TRESHOLD);
          shortQueue.push({funct:executePromiseQueueSync, argmnt:[temparray]});
        }
        return executePromiseQueueSync(shortQueue);
    }

    var seed = $.Deferred(); 
    var acc = seed;

    for (var i = 0; i < queue.length; ++i )
    {
         var promise = queue[i];
         acc = acc.then(promise.funct.apply(null, promise.argmnt));
    }

    seed.resolve();
    return acc;
}

所以基本思想是制作一个promise树而不是promise链。这样我们就不会深入堆栈。

示例:http://jsfiddle.net/fMBJK/1/

【问题讨论】:

  • 为什么promise.funct 没有返回函数时会被调用?看起来你正在向.then() 传递承诺,这是无效的
  • 如果someTask 是同步的,为什么还要使用承诺?
  • @Bergi - 您可以从 someTask() 中删除承诺,问题仍然存在。
  • 是否需要使用promise?还是只是想在等待上一个函数完成的同时按顺序执行链?
  • @Bergi - 假设您想按顺序执行一些 ajax 调用。这个例子对你来说足够了吗?

标签: javascript algorithm recursion promise jquery-deferred


【解决方案1】:

您可以使用jQuery.when ~ 虽然传入超过 200 个参数可能有点疯狂:

http://jsfiddle.net/6j6em/1/

小提琴的代码

JavaScript 与 jQuery

var resolves = [];

function log(msg){
  $('#log').append('<div><small>' + msg + '</small></div>');
}

function someTask(i){
  var dfd = new $.Deferred();
  log('created task '+i);
  resolves.push(function(){
    log('resolved task '+i);
    dfd.resolve();
  });
  return dfd.promise();
}

$(function(){

  $('#resolve1').click(function(){
    for ( var i=0; i<resolves.length; i++ ) {
      resolves[i]();
    }
  });

  $('#resolve2').click(function(){
    for ( var i=0; i<resolves.length; i++ ) {
      if ( i == 5 ) continue;
      resolves[i]();
    }
  });

  $('#resolve3').click(function(){
    resolves[5]();
  });

  var i, queue = []; for(i=0; i<200; ++i){ queue.push(someTask(i)); }

  (jQuery.when.apply(jQuery, queue))
    .done(function(){
      log('all resolved!');
      alert('all resolved!');
    })
  ;

});

此示例的标记

<button id="resolve1">Resolve all</button>
&nbsp;&nbsp;
<button id="resolve2">Resolve (all but one)</button>
<button id="resolve3">Resolve (the remaining one)</button>
<div id="log"></div>

此示例的 CSS

#log {
    border: 1px solid black; 
    padding: 10px; 
    width: 400px; 
    height: 200px; 
    overflow: auto;
}


说明

以上大部分代码只是为了说明,重点是:

  var i, queue = []; for(i=0; i<200; ++i){ queue.push(someTask(i)); }

  (jQuery.when.apply(jQuery, queue))
    .done(function(){
      log('all resolved!');
      alert('all resolved!');
    })
  ;

上面生成了一个promise对象数组,然后使用apply将它们传递给jQuery.when,然后处理创建正确的结构以在它们全部完成后触发done回调。也就是说,如果你想要这种行为。如果您想要一个系统等待每个 Promise 对象按顺序解析,然后再触发下一个任务,您将需要一些不同的东西。


更新

就按顺序执行任务而言,您需要一种不同的方法,如下所示:

http://jsfiddle.net/6j6em/2/

function log(msg){
  $('#log').append('<div><small>' + msg + '</small></div>');
}

function someTask(i){
  var dfd = new $.Deferred();
  log(':: created task '+i);
  setTimeout(function(){
    log(':: resolved task '+i);
    dfd.resolve();
  },50);
  return dfd.promise();
}

$(function(){

  var Queue;

  Queue = function( items ){ 
    this.items = items.slice(0);
    this.promise = $.Deferred();
  };
  Queue.prototype.next = function(){
    log(':: next task triggered');
    var q = this;
    q.lastItem = q.items.shift();
    if ( q.lastItem ) {
      q.lastPromise = q.lastItem.func.apply( null, q.lastItem.args );
      q.lastPromise.then(function(){
        /// include a setTimeout 0 to avoid possible stack/recursion errors.
        setTimeout(function(){
          q.next();
        },0);
      });
    }
    else {
      /// we are finished
      q.promise.resolve();
    }
  };
  Queue.prototype.run = function(){
    this.next();
  };

  var i, items = []; for(i=0; i<200; ++i){ 
    items.push({ func: someTask, args:[i] });
  }

  var q = new Queue( items );
  q.promise.done(function(){
    log(':: done!');
    alert('Done!');
  });
  q.run();

});

这会构建一个定制的Queue 对象,用于跟踪承诺列表,并在第一个成功后触发下一个。然而,这段代码显然需要错误处理。


更新 x2

您不能依赖每个 Promise 的进度,因为任何时候只有一个被触发。但是,您可以将自己的通知调用添加到整个 $.Deferred() 对象。

var Queue;
Queue = function( items ){ 
  this.items = items.slice(0);
  this.promise = $.Deferred();
  this.count = 0;
};
Queue.prototype.next = function(){
  log(':: next task triggered');
  var q = this;
  q.lastItem = q.items.shift();
  if ( q.lastItem ) {
    q.lastPromise = q.lastItem.func.apply( null, q.lastItem.args );
    q.lastPromise.then(function(){
      q.promise.notify(q.count++);
      q.next();
    });
  }
  else {
    q.promise.resolve();
  }
};
Queue.prototype.run = function(){
  this.next();
};
var q = new Queue( items );
q.promise
  .done(function(){log(':: done!');})
  .progress(function(p){log('::progress ' + p);})
;
q.run();

【讨论】:

  • @mnowotka ~ 不,不是,您通常使用 Promise,因为您不知道事情发生的顺序。您的问题不清楚这是必需的。我需要用另一种方法进行更新。
  • @mnowotka ~ 我添加了更新。此代码将按顺序触发任务。
  • 实际上,即使您删除 setTimeout,它似乎也可以工作。不错!
  • @mnowotka ~ 问题是因为你本质上是一个接一个地触发一个任务/承诺,每个承诺的进度都无法追踪。您可以增强上面的代码,使其重新用于整个 Queue 对象,但是......请参阅我的编辑。
  • 我整理出来了。一切都像魅力一样。非常感谢!
【解决方案2】:

FWIW...我有点晚了,但这是我对您的队列要求的实现:

function RunnablePromise(func, args, thisArg) {
    var def = $.Deferred(), prm = def.promise();

    prm.run = function () {
        func.apply(thisArg, args).done(def.resolve).fail(def.reject);
        return prm;
    };
    return prm;
}

function PromiseQueue() {
    var q = [], overall = $.Deferred(), self = this;

    this.run = function () {
        var runnable = q.shift();
        if (runnable) {
            overall.notify(q.length);
            runnable.run().done(self.run).fail(overall.reject);
        } else {
            overall.resolve();
        }
        return overall.promise();
    };
    this.append = function (task, args, thisArg) {
        var runnable = new RunnablePromise(task, args, thisArg);
        q.push(runnable);
        return runnable;
    };
}

这样使用:

var pq = new PromiseQueue(), i,
    success = function (i) { console.log("task done: " + i); };

for (i = 0; i < 200; i++) {
    // .append() returns the individual task's promise
    pq.append(someAsyncTask, [i]).done(success);
}

// .run() returns the entire queue's promise
pq.run().progress(function (remain) {
    console.log("remaining: " + remain);
}).done(function () {
    console.log("all done!");
}).fail(function () {
    console.log("error!");
});

http://jsfiddle.net/CxNDv/

【讨论】:

  • 感谢您的解决方案,您能评论一下吗?为什么它比接受的更好?
  • 嗯,一方面,它更短。 ;) 我也认为它更具可读性;概念大致相同。但最终:我在作品中有一个答案,还没写完就被打断了,但又不想因为迟到就扔掉。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多