循环中异步函数的调用层次结构?

7
有一个异步调用,我正在对一个服务上的数据库进行查询,但是这个服务一次输出的数量有限制,所以我需要通过它发送的结果来检查是否达到了限制,并重复查询直到没有达到限制。
同步模拟:
var query_results = [];

var limit_hit = true; #While this is true means that the query hit the record limit
var start_from = 0; #Pagination parameter

while (limit_hit) {
    Server.Query(params={start_from : start_from}, callback=function(result){
        limit_hit = result.limit_hit;
        start_from = result.results.length;
        query_result.push(result.results);
    }
}

显然,上述方法不起作用。我在这里看到了一些关于此问题的其他问题,但它们没有提及当您需要每次迭代等待上一个迭代完成并且您不事先知道迭代次数时该怎么办。
我如何将上述内容变为异步?我可以接受使用承诺/延迟逻辑的答案,但最好是整洁的解决方案。
我可能会想出一种可怕的方式来使用等待/超时来解决这个问题,但一定有一种干净,聪明和现代的方法来解决它。
另一种方法是进行“预查询”以事先了解要素的数量,因此您知道循环的数量,但我不确定这是否是正确的方法。
我们有时使用Dojo,但我找到的示例没有解释当您有未知数量的循环时该怎么做。https://www.sitepen.com/blog/2015/06/10/dojo-faq-how-can-i-sequence-asynchronous-operations/

使用递归方法而不是循环。这将与 Promises 自然地流动。 - Bergi
5个回答

4
虽然已经有许多答案,但我仍然认为 async/await 是最简洁的方式。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

而且你可能需要使用Babel

https://babeljs.io/

JS异步逻辑语法从回调(callback)变成了Promise,然后再到async/await,它们都做同样的事情。当callback嵌套很多时,我们需要像链一样的东西,于是就有了Promise。当Promise进入循环时,我们需要使链更加简单明了,于是就有了async/await。但并非所有浏览器都支持新的语法,因此babel编译器应运而生,将新语法编译为旧语法,这样你就可以始终使用新语法编写代码。

getData().then((data) => {
  //do something with final data
})

async function getData() {
  var query_results = [];

  var limit_hit = true;
  var start_from = 0;
  
  //when you use await, handle error with try/catch
  try {
    while (limit_hit) {
      const result = await loadPage(start_from)
      limit_hit = result.limit_hit;
      start_from = result.results.length;
      query_result.push(result.results);
    }
  } catch (e) {
    //when loadPage rejects
    console.log(e)
    return null
  }
  return query_result
}

async function loadPage(start_from) {
  //when you use promise, handle error with reject
  return new Promise((resolve, reject) => Server.Query({
    start_from
  }, (result, err) => {
    //error reject
    if (err) {
      reject(err)
      return
    }
    resolve(result)
  }))
}


哇,这很不一样,我会研究一下并尝试理解的,谢谢! - Mojimi
你可以在示例中添加一下,我应该把always/failure回调函数放在哪里? - Mojimi

3

如果您想使用循环,我认为没有(干净的)方法可以在不使用Promises的情况下完成。

另一种方法是以下内容:

var query_results = [];

var start_from = 0;

funciton myCallback(result) {  
  if(!result) {
    //first call
    Server.Query({ start_from: start_from}, myCallback);
  } else {
    //repeated call
    start_from = result.results.length
    query_result.push(result.results);
    
    if(!result.limit_hit) {
      //limit has not been hit yet
      //repeat the query with new start value
      Server.Query({ start_from: start_from}, myCallback);
    } else {
        //call some callback function here
    }
  }
}

myCallback(null);

您可以将其称为递归,但由于查询是异步的,因此您不应该遇到调用堆栈限制等问题。

在ES6环境中使用Promise,您可以使用async/await。我不确定是否可以在dojo中实现这一点。


如果它很干净,我可以接受 Promise,据我所知,Dojo 是建立在 ES6 Promise 之上的。 - Mojimi
我理解了你的例子,但是没有办法知道什么时候做对了,对吧? - Mojimi
我以为当 results.hit_limit 为 true 时就完成了?我有什么误解吗? - Lukilas
我的意思是,当递归完成时回调。 - Mojimi

2
你写过速率限制器或队列之后才会理解回调函数的含义;关键在于使用计数器:在异步请求之前增加计数器,在获取响应后减少计数器,这样就能知道有多少请求处于"进行中"状态。
如果服务器被阻塞,你需要将项目放回队列中。
你需要考虑许多因素: 如果进程被终止,队列会发生什么情况? 发送另一个请求前要等多久? 确保回调函数不会被多次调用! 重试多少次? 放弃前要等多久? 确保没有任何漏洞!(回调函数永远不会被调用)
当考虑到所有边缘情况时,你将得到一个相当冗长且不太优雅的解决方案。但是你可以将其抽象成一个函数!(返回一个Promise或其他你喜欢的东西)。 如果你有用户界面,你也想显示加载条和一些统计数据!

0

每次都必须等待服务器响应。这里有一个封装的方法

var query = (function(){
  var results = [];
  var count = 0;
  return function check(fun){
    Server.Query({ start_from: count}, function(d){
      count = d.results.length;
      results.push(d.results);
      if (d.limit_hit && fun) fun(results);
      else check(fun);
    });
  };
})();

// Call here
var my_query = query(function(d){
  // --> retrive all data when limit_hit is true)
});

-1

您可以使用生成器函数生成器来实现此目的 对于POC:

一些基础知识: - 您可以使用星号*定义一个生成器 - 它会暴露一个next函数,该函数返回下一个值 - 生成器可以通过内部的yield语句暂停,并且可以通过调用next()来从外部恢复 - While (true)将确保生成器在达到limit之前不会完成

function *limitQueries() {
  let limit_hit = false;
  let start_from  = 0;
  const query_result = [];

  while (true) {
    if (limit_hit) {break;}
    yield Server.Query(params={start_from : start_from}, 
        callback=function* (result) {
        limit_hit = result.limit_hit;
        start_from = result.results.length;
        yield query_result.push(result.results);
    }
  }
}

显然,生成器函数维护其自己的状态。生成器函数公开两个属性 { value, done },您可以这样调用它

const gen = limitQueries();
let results = [];
let next = gen.next();

while(next.done) {
  next = gen.next();
}
results = next.value;

你可能需要修改你的Server.Query方法来处理生成器回调。希望这能帮到你!干杯!


1
不,当你实际需要 async/await 时,不应该使用生成器。你的代码根本就不起作用 - 你立即消耗了所有东西,它进入了一个无限循环,因为你从未分配给 limit_hit,而且我甚至无法确定那个全局的 callback 变量应该做什么。 - Bergi

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