JavaScript: while循环中的异步方法

23

我正在处理一个项目,需要我使用JavaScript进行API方法调用。 我是一名Java程序员,以前从未进行过Web开发,所以我遇到了一些问题。

这个API方法是异步的,并且它在while循环中。如果它返回一个空数组,那么while循环就会结束。否则,它会一直循环。 代码:

var done = true;

do
{
    async_api_call(
        "method.name", 
        { 
            // Do stuff.
        },
        function(result) 
        {
            if(result.error())
            {
                console.error(result.error());
            }
            else
            {
                // Sets the boolean to true if the returned array is empty, or false otherwise.
                done = (result.data().length === 0) ? true : false;
            }
        }
    );

} while (!done);

这个不起作用。循环结束之前,“done”的值没有被更新。我已经阅读了一些关于这个主题的资料,似乎我需要使用promise或回调,因为API调用是异步的,但我无法理解如何将它们应用到上面的代码中。

帮助将不胜感激!


1
你真的需要使用while循环吗?异步编程的思想是避免循环直到完成某些操作,而是使用回调函数(在你的情况下为function(result))来更新UI。 - Erik Terwan
你的 async_api_call 已经被触发了,所以它会通过,不会等待回调。这不就是异步调用的目的吗?先阅读这篇文章:https://dev59.com/6XRB5IYBdhLWcg3wAjTR ,也许它能澄清异步函数的含义。因为你还不能更新 done 变量,所以 !done 是假的,并且打破了 do while 循环。 - Dinca Adrian
我需要循环多次调用async_api_call。这是因为所涉及的方法只能批处理50个数据,而我需要处理成千上万个项目,因此我需要继续循环调用它,直到所有项目都被处理完毕。 - Step
重要的是要理解,内部函数(function(result))将在某个稍后的时间点执行,可能是在父函数执行完成之后很长一段时间;async_api_call不会阻塞直到异步操作完成,然后再执行回调,它会更早地返回并在工作完成时执行回调。已经有一些有帮助的答案了,只是想要澄清这一点。 - Alex Paven
你能在这里使用ES6吗? - Eduard Jacko
只是一个小建议。你可以简单地这样做:done = result.data().length === 0,而不是在其后面加上 ? true : false - J.Ko
9个回答

18

编辑:请看底部,那里有真正的答案。

我建议您使用Promise API。您的问题可以通过调用Promise.all来解决:

let promises = [];
while(something){
    promises.push(new Promise((r, j) => {
        YourAsyncCall(() => r());
    });
}
//Then this returns a promise that will resolve when ALL are so.
Promise.all(promises).then(() => {
    //All operations done
});

这里的语法是es6,以下是相应的es5代码(Promise API可能需要从外部导入):

var promises = [];
while(something){
    promises.push(new Promise(function(r, j){
        YourAsyncCall(function(){ r(); });
    });
}
//Then this returns a promise that will resolve when ALL are so.
Promise.all(promises).then(function(){
    //All operations done
});

您可以使您的API调用返回Promise并将其直接推入Promise数组中。

如果您不想编辑api_call_method,则可以始终在新的promise中包装您的代码,并在完成后调用方法resolve。

编辑:我现在看到了您代码的重点,抱歉。我刚意识到Promise.all不能解决问题。

您应该将您发布的内容(不包括while循环和控制值)放在一个函数内,并根据条件再次调用它。

然后,可以将所有内容都包装在Promise中,以使外部代码意识到这种异步执行。稍后我会在我的电脑上发布一些示例代码。

所以正确的答案是

您可以使用Promise来控制应用程序的流程,并使用递归代替while循环:

function asyncOp(resolve, reject) {
    //If you're using NodeJS you can use Es6 syntax:
    async_api_call("method.name", {}, (result) => {
      if(result.error()) {
          console.error(result.error());
          reject(result.error()); //You can reject the promise, this is optional.
      } else {
          //If your operation succeeds, resolve the promise and don't call again.
          if (result.data().length === 0) {
              asyncOp(resolve); //Try again
          } else {
              resolve(result); //Resolve the promise, pass the result.
          }
      }
   });
}

new Promise((r, j) => {
    asyncOp(r, j);
}).then((result) => {
    //This will call if your algorithm succeeds!
});

/*
 * Please note that "(...) => {}" equivals to "function(...){}"
 */

2
如果你的目标是ES6,那么请使用async await。这里有太多层了。 - Eduard Jacko
1
@EduardJacko 是的,async/await非常适合处理这种情况,但当答案被创建时,我还不知道它。 - Sigma Octantis
实际上,他的async_api_call接受回调函数。无论如何你都不能使用async await。抱歉。 - Eduard Jacko

11

sigmasoldier解决方案 是正确的,我只是想分享带有 async / await 的 ES6 版本:

const asyncFunction = (t) => new Promise(resolve => setTimeout(resolve, t));

const getData = async (resolve, reject, count) => {

    console.log('waiting');
    await asyncFunction(3000);
    console.log('finshed waiting');

    count++;

    if (count < 2) {
        getData(resolve, reject, count);
    } else {
        return resolve();
    }
}

const runScript = async () => {
    await new Promise((r, j) => getData(r, j, 0));
    console.log('finished');
};

runScript();


很好,这指引了我需要做的事情。在我的特定情况下,我需要“循环”异步调用,但每个连续的调用都依赖于前一个调用返回的一些数据。添加一些变量并根据我的需求修改计数,我让它工作了。唯一让我不太理解的是必须将getData()本身包装在一个promise中,如果getData本身返回一个promise(因为是async func),为什么我们不能直接调用它并期望相同的结果? - xunux
我认为你是对的 @xunux。既然异步函数本身应该返回一个 Promise,所以没有必要再使用 Promise。 - tim
这是对我来说有意义的,但我需要这样做才能使它工作,我认为需要从原始包装承诺中传递解析和拒绝函数,因为即使await getData()返回一个承诺,我也无法传递所需的解析和拒绝函数以结束每个循环。 - xunux

9

如果您不想使用递归,可以将您的while循环改为for of循环,并使用生成器函数来维护完成状态。这是一个简单的示例,其中for of循环将等待异步函数,直到我们进行了5次迭代,然后将done翻转为true。当您的webservice调用缓冲了所有数据行时,应该能够更新此概念以将done变量设置为true。

let done = false;
let count = 0;
const whileGenerator = function* () {
    while (!done) {
        yield count;
    }
};

const asyncFunction = async function(){
    await new Promise(resolve => { setTimeout(resolve); });
};
const main = new Promise(async (resolve)=>{
    for (let i of whileGenerator()){
       console.log(i);
       await asyncFunction();

       count++;
       if (count === 5){
           done = true;
       }
    }
    resolve();
});
main.then(()=>{
    console.log('all done!');
});

请注意,此解决方案是有状态的。这意味着您不能在多个for循环中共享状态变量和生成器,否则可能会出现问题。 - Zambonilli

3
您可以尝试使用递归解决方案。
function asyncCall(cb) {
// Some async operation
}

function responseHandler(result) {
    if (result.error()) {
        console.error(result.error());
    } else if(result.data() && result.data().length) {
        asyncCall(responseHandler);
    }
}

asyncCall(responseHandler);

2

这是我想出的一个解决方案。将其放置在异步函数中。


        let finished = false;
        const loop = async () => {
            return new Promise(async (resolve, reject) => {
                const inner = async () => {
                    if (!finished) {
                        //insert loop code here
                        if (xxx is done) { //insert this in your loop code after task is complete
                           finshed = true;
                           resolve();
                        } else {
                           return inner();
                        }
                    }
                }
                await inner();
            })
        }
        await loop();

1
如果您不想使用 Promises,可以像这样重新构造您的代码:
var tasks = [];
var index = 0;

function processNextTask()
{
    if(++index == tasks.length)
    {
        // no more tasks
        return;
    }

    async_api_call(
        "method.name", 
        { 
            // Do stuff.
        },
        function(result) 
        {
            if(result.error())
            {
                console.error(result.error());
            }
            else
            {
                // process data
                setTimeout(processNextTask);
            }
        }
    );
}

0

你的循环不会起作用,因为它是同步的,而你的异步任务是异步的,所以在异步任务能够响应之前,循环就已经完成了。我建议你使用 Promises 来管理异步任务:

//first wrapping your API into a promise
var async_api_call_promise = function(methodName, someObject){
    return new Promise((resolve, reject) => {
        async_api_call(methodName, someObject, function(result){
            if(result.error()){ 
                reject( result.error() ) 
            }else{
                resolve( result.data() )
            }
        });
    })
}

现在来看你的投票代码:

//a local utility because I don't want to repeat myself
var poll = () => async_api_call_promise("method.name", {/*Do stuff.*/});

//your pulling operation
poll().then(
    data => data.length === 0 || poll(),  //true || tryAgain
    err => {
        console.error(err);
        return poll();
    }
).then((done) => {
    //done === true
    //here you put the code that has to wait for your "loop" to finish
});

为什么要使用Promise?因为它们能够管理异步操作的状态。为什么要自己实现呢?


0
  let taskPool = new Promise(function(resolve, reject) {
    resolve("Success!");
  });
  let that = this;
  while (index < this.totalPieces) {
    end = start + thisPartSize;
    if (end > filesize) {
      end = filesize;
      thisPartSize = filesize - start;
    }
    taskPool.then(() => {
      that.worker(start, end, index, thisPartSize);
    });
    index++;
    start = end;
  }

0
我实际上将一个 while 循环转换为 for 循环,而不是使用生成器的其他答案。这样做更简洁,更直观地展示了发生的情况,而无需额外的状态。
const asyncFunction = async ()=>{
  await new Promise(resolve => { setTimeout(resolve); });
}

(async()=>{
  let i = 0;
  for (;;){
    await asyncFunction();
    
    console.log(i);
    // you have to manually perform your while condition in the loop
    if (i === 5) {
      // condition is true so break the infinite for loop
      break;
    }
    i++;
  }
  console.log('done');
})();

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