【问题标题】:Async promises recursion with unknow supply of async values异步承诺递归与异步值的未知供应
【发布时间】:2019-11-04 01:17:50
【问题描述】:

我正在研究异步承诺递归。我有一堆可以在下载异步数据时解决的承诺(由 Promise.all 组合)。但有时在我刚刚下载的数据中有指向另一个数据的链接,必须是下载(递归)。最好的解释是显示我猜的代码。注释在代码中。 (我尝试了各种组合都无济于事。)

var urls = ['http://czyprzy.vdl.pl/file1.txt', 'http://czyprzy.vdl.pl/file2.txt', 'http://czyprzy.vdl.pl/file3.txt'];
var urlsPromise = [];
var secondPart = [];
var thirdPart = [];

function urlContent(url, number) {
  return new Promise(function (resolve) {
    var dl = request(url, function (err, resp, content) {
      if (err || resp.statusCode >= 400) {
        return resolve({number : number, url : url, error : 'err'});
      }
      if (!err && resp.statusCode == 200) {
        if (content.indexOf('file') !== -1) // if there is 'file' inside content we need (would like to :) download this new file by recursion
        {
            content = content.slice(content.indexOf('file') + 4);
            content =+ content; // (number to pass later on, so we know what file we are working on)
            url = 'http://czyprzy.vdl.pl/file' + content + '.txt'; // (we build new address)

            //urlsPromise.push(urlContent(url, content)); // this will perform AFTER Promise.all(urlsPromise) so we simply can't do recurention (like that) here
            secondPart.push(urlContent(url, content)); // if we use another promise array that put resolved items to that array everything will work just fine - but only till first time, then we would need to add another (thirdPart) array and use another Promise.all(thirdPart)... and so on and so on... --- the problem is I don't know how many files there will be, so it means I have no idea how many 'parts' for Promise.all I need to create, some kind of asynchronous loop/recursion would save me here, but I don't know how to do that properly so the code can run in proper order
        }
        return resolve({number : number, url : url}); // this goes to 'urlsPromise' array
      }
    });
  });
}

if (urls.length !== 0) {
  for (var i = 0; i < urls.length; i++)
  {urlsPromise.push(urlContent(urls[i], i + 1));}
}

Promise.all(urlsPromise).then(function(urlsPromise) {
  console.log('=======================================');
  console.log('urlsPromise:\n');
  console.log(urlsPromise); // some code/calculations here
}).then(function() {
  return Promise.all(secondPart).then(function(secondPart) {
    console.log('=======================================');
    console.log('secondPart:\n');
    console.log(secondPart); // some code/calculations here
    secondPart.forEach(function(item)
    {
        thirdPart.push(urlContent(item.url, item.number + 3));
    });
  });
}).then(function() {
  return Promise.all(thirdPart).then(function(thirdPart) {
    console.log('=======================================');
    console.log('thirdPart:\n');
    console.log(thirdPart); // some code/calculations here
  });
}).then(function()
{
    console.log();
    console.log('and so on and so on...');
});



//// files LINKING (those files do exist on live server - just for testing purposes):
// file1->file4->file7->file10  /-/ file1 content: file4 /-/ file4 content: file7 /-/ file7 content: file10
// file2->file5->file8->file11  /-/ file2 content: file5 /-/ file5 content: file8 /-/ file8 content: file11
// file3->file6->file9->file12  /-/ file3 content: file6 /-/ file6 content: file9 /-/ file9 content: file12



//// the console.log output looks like this:
// =======================================
// urlsPromise:

// [ { number: 1, url: 'http://czyprzy.vdl.pl/file4.txt' },
//   { number: 2, url: 'http://czyprzy.vdl.pl/file5.txt' },
//   { number: 3, url: 'http://czyprzy.vdl.pl/file6.txt' } ]
// =======================================
// secondPart:

// [ { number: 4, url: 'http://czyprzy.vdl.pl/file7.txt' },
//   { number: 5, url: 'http://czyprzy.vdl.pl/file8.txt' },
//   { number: 6, url: 'http://czyprzy.vdl.pl/file9.txt' } ]
// =======================================
// thirdPart:

// [ { number: 7, url: 'http://czyprzy.vdl.pl/file10.txt' },
//   { number: 8, url: 'http://czyprzy.vdl.pl/file11.txt' },
//   { number: 9, url: 'http://czyprzy.vdl.pl/file12.txt' } ]

// and so on and so on...

【问题讨论】:

  • async/await 关键字可能会有所帮助,如果您能够使用它的话。它使使用 Promise 变得更加容易和可读。
  • 我想在这种特殊情况下我不能。但是一个使用 async/await 的示例(最好在这个问题代码示例中使用)将帮助我使用 Promise 编写类似的代码(我猜,因为 async 只是 Promise 的合成糖)。

标签: javascript asynchronous recursion promise


【解决方案1】:

await 关键字可以大大简化这一点。您不需要使用自递归函数。此演示使用随机大小的数组伪造服务器调用。

https://jsfiddle.net/mvwahq19/1/

// setup: create a list witha  random number of options.
var sourceList = [];
var numItems = 10 + Math.floor(Math.random() * 20);

for (var i = 0; i < numItems; i++)
{
    sourceList.push(i);
}

sourceList.push(100);

var currentIndex = 0;

// a function which returns a promise. Imagine it is asking a server.
function getNextItem() {
    var item = sourceList[currentIndex];
    currentIndex++;
    
    return new Promise(function(resolve) {
        setTimeout(function() {
          resolve(item);
        }, 100);
    });
}

async function poll() {
    var collection = [];
    var done = false;
    while(!done) {
        var item = await getNextItem();
        
        collection.push(item);
        
        console.log("Got another item", item);
        
        if (item >= 100) {
            done = true;
        }
    }
    console.log("Got all items", collection);
}

poll();

你可以写一个普通的for循环,除了内容使用await。

【讨论】:

    【解决方案2】:

    感谢 trincot - https://stackoverflow.com/users/5459839/trincot

    提供此答案

    当我直接问他这个问题时,他用时间和知识支持我,并给出了很好的答案。

    代码:

    //// files LINKING (those files do exist on live server - just for testing purposes):
    // file1->file4(AND file101)->file7->file10  /-/ file1 content: file4 /-/ file4 content: file7 /-/ file7 content: file10 /-/ file10 content: EMPTY /-/ file101 content: EMPTY
    // file2->file5(AND file102)->file8->file11  /-/ file2 content: file5 /-/ file5 content: file8 /-/ file8 content: file11 /-/ file11 content: EMPTY /-/ file102 content: EMPTY
    // file3->file6(AND file103)->file9->file12  /-/ file3 content: file6 /-/ file6 content: file9 /-/ file9 content: file12 /-/ file12 content: EMPTY /-/ file103 content: EMPTY
    
    var urls = ['http://czyprzy.vdl.pl/file1.txt', 'http://czyprzy.vdl.pl/file2.txt', 'http://czyprzy.vdl.pl/file3.txt'];
    var urlsPromise = [];
    
    function requestPromise(url) {
        return new Promise(function(resolve, reject) {
            request(url, function (err, resp, content) {
                if (err || resp.statusCode != 200) reject(err || resp.statusCode);
                else resolve(content);
            });
        });
    }
    
    async function urlContent(url, number) {
      var arr = [];
      let content = await requestPromise(url);
    
      while (content.indexOf(';') !== -1)
      {
        var semiColon = content.indexOf(';');
    var fileLink = content.slice(content.indexOf('file'), semiColon + 1);
    content = content.replace(fileLink, ''); // we need to remove the file link so we won't iterate over it again, we will add to the array only new links
    var fileLinkNumber = fileLink.replace('file', '');
    fileLinkNumber = fileLinkNumber.replace(';', '');
    fileLinkNumber =+ fileLinkNumber;
    url = 'http://czyprzy.vdl.pl/file' + fileLinkNumber + '.txt'; // we build new address
    arr.push({url, fileLinkNumber});
    }
    if (content.indexOf('file') !== -1)
    {
    var fileLinkNumber = content.slice(content.indexOf('file') + 4);
    fileLinkNumber =+ fileLinkNumber;
      url = 'http://czyprzy.vdl.pl/file' + fileLinkNumber + '.txt';
      arr.push({url, fileLinkNumber});
    }
    var newArr = arr.map(function(item)
    {
    return urlContent(item.url, item.fileLinkNumber); // return IS important here
    });
    return [].concat(arr, ...await Promise.all(newArr));
    }
    
    async function doing() {
        let urlsPromise = [];
        for (let i = 0; i < urls.length; i++) {
            urlsPromise.push(urlContent(urls[i], i + 1));
        }
        let results = [].concat(...await Promise.all(urlsPromise)); // flatten the array of arrays
        console.log(results);
    }
    
    //// this is only to show Promise.all chaining - so you can do async loop, and then wait for some another async data - in proper chain.
    
    var test_a = ['http://czyprzy.vdl.pl/css/1.css', 'http://czyprzy.vdl.pl/css/2.css', 'http://czyprzy.vdl.pl/css/cssa/1a.css', 'http://czyprzy.vdl.pl/css/cssa/2a.css'];
    var promisesTest_a = [];
    
    function requestStyle(url)
    {
    return new Promise(function(resolve, reject)
    {
    request(url, function(error, response, content)
    {
    if (response.statusCode === 200 && !error)
    {resolve(content);}
    else
    {reject(error);}
    });
    });
    }
    
    for (var i = 0; i < test_a.length; i++)
    {promisesTest_a.push(requestStyle(test_a[i]));}
    
    Promise.all(promisesTest_a).then(function(promisesTest_a)
    {
       console.log(promisesTest_a);
    }).then(function()
    {
       console.log('\nNow we start with @imports...\n');
    }).then(function()
    {
       return doing();
    }).then(function()
    {
       console.log('ALL DONE!');
    });
    

    评论:

    首先解释什么是 [...] - 解构的剩余参数(以防万一你不知道)。

    var arr = [];
    var array1 = ['one', 'two', 'three']
    var array2 = [['four', 'five', ['six', 'seven']], 'eight', 'nine', 'ten'];
    arr = array1.concat(array2);
    console.log(arr); // it does not flattern the array - it just concatenate them (join them together)
    console.log('---');
    // however
    arr = array1.concat(...array2);
    console.log(arr); // notice the [...] - as you can see it flatern the array - 'four' and 'five' are pull out of an array - think of it as level up :) remember that it pull up WHOLE array that is deeper - so 'six' and 'seven' are now 1 level deep (up from 2 levels deep, but still in another array).
    console.log('---');
    // so
    arr = [].concat(...arr);
    console.log(arr); // hurrrray our array is flat (single array without nested elements)
    console.log();
    

    所有准备下载的文件(链接)(urls 数组中的 3 个起始文件)几乎立即下载(在包含它们的数组上同步循环 - 一个接一个,但是非常快,很快,因为我们只是以同步方式迭代它们)。

    然后,当我们获得它们的内容时(因为我们等待内容被下载 - 所以我们在这里得到了一个已解决的承诺数据)我们开始寻找与我们已经获得的相关的其他可能的 url(文件)的信息,以下载它们(通过异步递归)。

    当我们找到关于可能的附加 url/文件的所有信息(以正则表达式数组 - matches 呈现)时,我们将其推送到数据数组(名为 arr 在我们的代码)并下载它们(感谢 url 的突变)。

    我们通过返回 async urlContent 函数来下载它们,该函数需要 Await 以获得 requestPromise 承诺(所以我们在 urlContent 中有解析/拒绝的数据,因此如果需要,我们可以对其进行变异 - 构建正确的 url 以获取下一个文件/内容)。

    以此类推,直到我们“迭代”(下载)所有文件。每次调用 urlContent 时,它都会返回一个初始未决的承诺数组(promises 变量)。当我们等待 Promise.all(promises) 时,只有在所有这些承诺都已解决后,才会在该位置恢复执行。因此,在那一刻,我们拥有每个承诺的价值。每一个都是一个数组。我们使用一个大的 concat 将所有这些数组一起整理成一个大数组,还包括 arr 的元素(我们需要记住,从我们已经下载的文件中下载的文件可以超过 1 个- 这就是为什么我们将值存储在数据数组中 - 在代码中命名为 arr - 它存储 promiseReques 函数解析/拒绝的值)。这个“大”数组是一个值,用它来解决一个承诺。回想一下,这个 Promise 是这个当前函数上下文已经返回的那个,在第一次 await 被执行的时候。

    这是重要的部分 - 所以它 (urlContent) 返回 (await) 单个 promise,并且 (返回的) promise 使用数组作为值来解析。请注意,当遇到第一个 await 时,异步函数会立即将 promise 返回给调用者。异步函数中的 return 语句决定了返回的 Promise 被解析的值是什么。在我们的例子中是一个数组。

    所以每次调用的 urlContent 都会返回一个 promise - 数组中的解析值 - [...](解构的剩余参数 - 返回一个最终解析为数组的 promise),这是由我们的异步 doing 函数(因为在开始时触发了 3 个 url - 每个都有它自己的 urlContent 函数...路径),它收集(等待!)来自 Promise.all(urlsPromise) 的所有这些数组,以及当它们被解析时(我们等待它们被 Promise.all 解析和传递)它“返回”你的数据(结果变量)。准确地说,doing 返回一个承诺(因为它是异步的)。但是我们称之为doing的方式,我们表明我们对这个promise解析为什么不感兴趣,事实上,由于doing没有return语句,这个promise解析为UNDEFINED(!)。无论如何,我们不使用它 - 我们只是将结果输出到控制台。

    与异步函数混淆的一件事是,当函数返回时,返回语句执行(名称中有什么,对吧!?;)。该函数在执行第一次等待时已经返回。当它最终执行 return 语句时,它并没有真正返回一个值,而是解析了“它自己的”promise;它之前返回的那个。如果我们真的想将输出与逻辑分开,我们不应该在那里做console.log(results),而是做返回结果,然后,在我们调用doing的地方,我们可以做doing.then(console.log);现在我们确实使用了doing返回的promise!

    我会保留动词“返回”来表示函数的调用者从它同步得到什么。 我将使用“to resolve”来将承诺设置为具有值的已解决状态,该值可以通过await访问> 或 .then().

    【讨论】:

      猜你喜欢
      • 2018-11-03
      • 2015-12-08
      • 2019-03-10
      • 2018-02-16
      • 2018-10-11
      • 1970-01-01
      • 2023-03-28
      • 2021-07-16
      相关资源
      最近更新 更多