在Node.js中延迟每个循环迭代,异步

27

我有以下代码:

var request = require('request');
var cheerio = require ("cheerio");
var async= require("async");

var MyLink="www.mylink.com";

    async.series([

        function(callback){
            request(Mylink, function (error, response, body) {
                if (error) return callback(error); 
                var $ = cheerio.load(body);
                //Some calculations where I get NewUrl variable...
                TheUrl=NewUrl;
                callback();
            });
        },
        function(callback){
            for (var i = 0; i <=TheUrl.length-1; i++) {
                var url = 'www.myurl.com='+TheUrl[i];
                request(url, function(error, resp, body) { 
                    if (error) return callback(error); 
                    var $ = cheerio.load(body);
                    //Some calculations again...
                    callback();
                });
            };
        }
      ], function(error){
        if (error) return next(error);
    });

有没有人能提供建议,关于如何在 for loop 中延迟每次循环迭代?比如说,代码在每次迭代完成后等待10秒钟。我尝试了setTimeout,但似乎没有生效。

5个回答

40

使用 async/await 延迟多个页面获取

我是 async 库的忠实粉丝,长期以来一直在使用它。但是,现在有了 async/await。你的代码变得更易读。例如,这将是您的主要函数:

const urls = await fetchUrls(INITIAL_URL);

for (const url of urls) {
    await sleep(10000);
    const $ = await fetchPage(url);
    // do stuff with cheerio-processed page
}

很好,是吧?在详细介绍fetchPage()fetchUrls()的工作原理之前,让我们先回答您关于如何在获取下一页之前等待的问题。睡眠函数非常简单:

async function sleep(millis) {
    return new Promise(resolve => setTimeout(resolve, millis));
}

您可以在我的另一个回答这里中获得有关其工作原理的完整说明。

好的,回到其他函数。 request库有一个启用了Promise的版本,您可以使用async/await与之配合使用。 让我们来看看fetchPage()的实现:

async function fetchPage(url) {
    return await request({
        url: url,
        transform: (body) => cheerio.load(body)
    });
}

由于request返回的是一个Promise,因此我们可以在其上使用await。我还利用了transform属性,它允许我们在解决Promise之前对响应体进行转换。我将其通过Cheerio传递,就像您在代码中所做的那样。

最后,fetchUrls()只需调用fetchPage()并处理它以获取URL数组,然后解决它的Promise。以下是完整的代码:

const
    request = require("request-promise-native"),
    cheerio = require("cheerio");

const
    INITIAL_URL = "http://your-initial-url.com";

/**
 * Asynchronously fetches the page referred to by `url`.
 *
 * @param {String} url - the URL of the page to be fetched
 * @return {Promise} promise to a cheerio-processed page
 */
async function fetchPage(url) {
    return await request({
        url: url,
        transform: (body) => cheerio.load(body)
    });
}

/**
 * Your initial fetch which will bring the list of URLs your looking for.
 *
 * @param {String} initialUrl - the initial URL
 * @return {Promise<string[]>} an array of URL strings
 */
async function fetchUrls(initialUrl) {
    const $ = await fetchPage(initialUrl);
    // process $ here and get urls
    return ["http://foo.com", "http://bar.com"];
}

/**
 * Clever way to do asynchronous sleep. 
 * Check this: https://dev59.com/1GYq5IYBdhLWcg3wui7F#46720712
 *
 * @param {Number} millis - how long to sleep in milliseconds
 * @return {Promise<void>}
 */
async function sleep(millis) {
    return new Promise(resolve => setTimeout(resolve, millis));
}

async function run() {
    const urls = await fetchUrls(INITIAL_URL);
    for (const url of urls) {
        await sleep(10000);
        const $ = await fetchPage(url);
        // do stuff with cheerio-processed page
    }
}

run();

要使用带有Promises的request,请按此安装:

npm install request
npm install request-promise-native

然后在您的代码中使用require("request-promise-native"),就像上面的示例一样。


30

您可以像这样设置增加间隔的代码执行超时时间:

var interval = 10 * 1000; // 10 seconds;

for (var i = 0; i <=TheUrl.length-1; i++) {
    setTimeout( function (i) {
        var url = 'www.myurl.com='+TheUrl[i];
        request(url, function(error, resp, body) { 
            if (error) return callback(error); 
            var $ = cheerio.load(body);
            //Some calculations again...
            callback();
        });
    }, interval * i, i);
}

因此第一个立即运行(interval * 0为0),第二个在十秒后运行,以此类推。

需要将i作为setTimeout()的最后一个参数发送,以使其值绑定到函数参数。否则,尝试访问数组值将超出范围,并且会得到undefined


1
为什么我不能把下一个最后一行从 }, interval * i, i); 改成 }, interval * 1, i);,从 i to 1? 如果我想要一个常数间隔,则代码就无法工作。 - user1665355
3
setTimeout() 立即返回,不会等待超时后才将控制权返回给 for 循环。因此,如果您使用 interval * 1(与仅使用 interval 相同),则所有内容都将在十秒钟后同时运行(或多或少——它们将相隔几毫秒,但仅如此)。但是,如果您使用 interval * i,则第一次通过会立即执行,第二次通过循环时将在十秒后执行,第三次通过时将在二十秒后执行,以此类推。 - Trott
好的,尝试了 interval*1 但没有结果... 但我明白你的意思!谢谢。 - user1665355
1
请注意,setTimeout有一个最大值,如果达到2147483647,它将会中断。 - Ali BAGHO

18

另一种选择是使用async.eachSeries。例如:

async.eachSeries(TheUrl, function (eachUrl, done) {
    setTimeout(function () {
        var url = 'www.myurl.com='+eachUrl;
        request(url, function(error, resp, body) { 
            if (error) return callback(error); 
            var $ = cheerio.load(body);
            //Some calculations again...
            done();
        });
    }, 10000);
}, function (err) {
    if (!err) callback();
});

10

既然您已经在使用asyncasync.whilst将很好地替代for

whilst是一个异步的类似于while的函数。每次迭代只有在前一次迭代调用其完成回调后才会运行。在这种情况下,我们可以使用setTimeout将完成回调的执行推迟10秒。

var i = 0;
async.whilst(
    // test to perform next iteration
    function() { return i <= TheUrl.length-1; },

    // iterated function
    // call `innerCallback` when the iteration is done
    function(innerCallback) {
        var url = 'www.myurl.com='+TheUrl[i];
        request(url, function(error, resp, body) { 
            if (error) return innerCallback(error); 
            var $ = cheerio.load(body);
            //Some calculations again...

            // wait 10 secs to run the next iteration
            setTimeout(function() { i++; innerCallback(); }, 10000);
        });
    },

    // when all iterations are done, call `callback`
    callback
);

能否在特定时间设置延迟?例如每30分钟一次? - user1665355

6
这里是一个提供 for 循环延迟的示例代码。
const sleep = (milliseconds) => {
    const date = Date.now();
    let currentDate = null;
    do {
      currentDate = Date.now();
    } while (currentDate - date < milliseconds);
};

for (let index = 0; index < 10; index++) {
    console.log(index);
    sleep(1000);
}

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