完成forEach后运行回调函数

时间:2017-06-08 23:35:11

标签: javascript asynchronous foreach callback

在项目中,我有一个循环浏览网址列表。它从每个URL下载文件,并对下载的文件进行一些后期处理。

完成所有过程(下载过程和后期过程)后,我想执行回调函数。由于后期处理包含一些流媒体任务,因此它具有关闭事件。如果可以识别最后一项,我可以将回调函数传递给close事件。但是,由于循环是异步的,我无法跟踪最终完成的项目。

现在,我使用5秒超时来确保在整个过程之后执行回调。显然,这是不可持续的。处理这个问题的好方法是什么?

循环代码:

exports.processArray = (items, process, callback) => {
    var todo = items.concat();
    setTimeout(function() {
        process(todo.shift());
        if(todo.length > 0) {
          // execute download and post process each second
          // however it doesn't guarantee one start after previous one done
          setTimeout(arguments.callee, 1000);
        } else {
          setTimeout(() => {callback();}, 5000);
        }
    }, 1000);
};

processArray(
  // First param, the array
  urlList,
  // Second param, download and post process
  (url) => {
    if(url.startsWith('http')) {
      getDataReg(url, uid);
    }
    else if(url.startsWith('ftp')) {
      getDataFtp(url, uid);
    }
    else {
      console.log('not a valid resource');
    }
  },
  // Third param, callback to be executed after all done
  () => {
    Request.get(`${config.demouri}bound=${request.query.boundary};uid=${uid}`, {
      method: 'GET',
      auth: auth
    })
    .on('response', (response) => {
      console.log('response event emmits');
      zipFiles(uid)
      .then((path) => {
        reply.file(path, { confine: false, filename: uid + '.zip', mode: 'inline'}).header('Content-Disposition');
      });
    });
  }
);

下载和发布流程:

exports.getDataFtp = (url, uid) => {
  console.log('get into ftp');
  var usefulUrl = url.split('//')[1];
  var spliter = usefulUrl.indexOf('/');
  var host = usefulUrl.substring(0, spliter);
  var dir = usefulUrl.substring(spliter+1, usefulUrl.length);
  var client = new ftp();
  var connection = {
    host: host
  };
  var fileNameStart = dir.lastIndexOf('/') + 1;
  var fileNameEnd = dir.length;
  var fileName = dir.substring(fileNameStart, fileNameEnd);
  console.log('filename: ', fileName);

  client.on('ready', () => {
    console.log('get into ftp ready');
    client.get(dir, (err, stream) => {
      if (err) {
        console.log('get file err:', err);
        return;
      } else{
        console.log('get into ftp get');
        stream.pipe(fs.createWriteStream(datadir + `download/${uid}/${fileName}`));
        stream.on('end', () => {
          console.log('get into ftp close');
          unzipData(datadir + `download/${uid}/`, fileName, uid);
          client.end();
        });
      }
    });
  });
  client.connect(connection);
};

exports.getDataReg = (url, uid) => {
  console.log('get into http');
    var fileNameStart = url.lastIndexOf('/') + 1;
  var fileNameEnd = url.length;
  var fileName = url.substring(fileNameStart, fileNameEnd);
    var file = fs.createWriteStream(datadir + `download/${uid}/${fileName}`);
    if (url.startsWith('https')) {
    https.get(url, (response) => {
      console.log('start piping file');
      response.pipe(file);
      file.on('finish', () => {
        console.log('get into http finish');
        unzipData(datadir + `download/${uid}/`, fileName, uid);
      });
    }).on('error', (err) => { // Handle errors
      fs.unlink(datadir + `download/${uid}/${fileName}`);
      console.log('download file err: ', err);
    });
    } else {
    http.get(url, (response) => {
      console.log('start piping file');
      response.pipe(file);
      file.on('finish', () => {
        unzipData(datadir + `download/${uid}/`, fileName, uid);
      });
    }).on('error', (err) => {
      fs.unlink(datadir + `download/${uid}/${fileName}`);
      console.log('download file err: ', err);
    });
    }
};

function unzipData(path, fileName, uid) {
  console.log('get into unzip');
  console.log('creating: ', path + fileName);
    fs.createReadStream(path + fileName)
    .pipe(unzip.Extract({path: path}))
    .on('close', () => {
    console.log('get into unzip close');
    var filelist = listFile(path);
    filelist.forEach((filePath) => {
      if (!filePath.endsWith('.zip')) {
        var components = filePath.split('/');
        var component = components[components.length-1];
        mv(filePath, datadir + `processing/${uid}/${component}`, (err) => {
          if(err) {
            console.log('move file err: ');
          } else {
            console.log('move file done');
          }
        });
      }
    });
    fs.unlink(path + fileName, (err) => {});
    });
}

3 个答案:

答案 0 :(得分:2)

  

完成所有过程(下载过程和后期过程)后,我想执行一个回调函数。

关于一系列异步流程的有趣之处在于,您永远无法知道何时完成所有流程。因此,为回调设置超时是快速而肮脏的方式,但肯定不可靠。

您可以使用counter来解决此问题。 假设您有10个操作要执行。在开始时,您将计数器设置为十counter = 10并且在每个过程完成后,无论如何(它可以成功还是失败),您可以将计数器递减1,如counter -= 1,紧随其后可以检查计数器是否为0,如果是,则表示所有进程都已完成,我们已到达结束。您现在可以安全地运行回调函数,例如if(counter === 0) callback();

如果我是你,我会做这样的事情:

*请注意,被调用的进程应返回一个promise,以便我知道它何时完成(无论如何)

*如果您需要有关承诺的帮助,这篇有用的文章可能会对您有所帮助:https://howtonode.org/promises

*哦,还有一件事,你应该避免使用arguments.callee,因为它已被弃用。这就是Why was the arguments.callee.caller property deprecated in JavaScript?

的原因
exports.processArray = (items, process, callback) => {
    var todo = [].concat(items);
    var counter = todo.length;

    runProcess();

    function runProcess() {
      // Check if the counter already reached 0
      if(checkCounter() === false) {
        // Nope. Counter is still > 0, which means we got work to do.
        var processPromise = process(todo.shift());

        processPromise
          .then(function() {
            // success
          })
          .catch(function() {
            // failure
          })
          .finally(function() {
            // The previous process is done. 
            // Now we can go with the next one.
            --counter;
            runProcess();
          })
      }
    };

    function checkCounter() {
      if(counter === 0) {
        callback();
        return true;
      } else {
        return false;
      }
    }
};

答案 1 :(得分:1)

您要做的是使所有异步进程聚合成一个可用于在正确的时刻执行回调的承诺。

让我们从每个进程完成的点开始,我假设在回传传递给mv()中的unzipData()函数。您希望将每个异步操作包装在一个在回调中解析的Promise中,并且您还希望稍后使用这些promise,并且为此使用.map()方法来收集数组中的promise(而不是{{ 1}})。
这是代码:

.forEach()

(如果不执行异步操作,则返回立即解析的Promise)

现在,我们可以将此Promise列表转换为单个Promise,当列表中的所有promise都已解决时,它将解析:

var promises = filelist.map((filePath) => {
  if (!filePath.endsWith('.zip')) {
    var components = filePath.split('/');
    var component = components[components.length-1];
    return new Promise((resolve, reject) =>
      mv(filePath, datadir + `processing/${uid}/${component}`, (err) => {
        if(err) {
          console.log('move file err: ');
          reject(); // Or resolve() if you want to ignore the error and not cause it to prevent the callback from executing later
        } else {
          console.log('move file done');
          resolve();
        }
      }));
  }
  return Promise.resolve();
});

接下来,我们需要在代码中进一步查看。我们可以看到,我们刚才看到的代码本身就是异步操作的事件处理程序的一部分,即var allPromise = Promise.all(promises); 。你需要将它包装在一个promise中,当内部promises解析时它会被解析,这就是fs.createReadStream()函数应该返回的承诺:

unzipData()

接下来,我们来看一下使用function unzipData(path, fileName, uid) { console.log('get into unzip'); console.log('creating: ', path + fileName); return new Promise((outerResolve) => fs.createReadStream(path + fileName) .pipe(unzip.Extract({path: path})) .on('close', () => { console.log('get into unzip close'); var filelist = listFile(path); // Code from previous examples allPromise.then(outerResolve); })); } 的函数:unzipData()getDataReg()。它们只执行一个异步操作,因此您需要做的就是让它们返回一个承诺,该承诺在getDataFtp()返回的承诺解析时解析。 简化示例:

unzipData()

最后,我们进入exports.getDataReg = (url, uid) => { return new Promise((resolve, reject) => { // ... https.get(url, (response) => { response.pipe(file); file.on('finish', () => { unzipData(datadir + `download/${uid}/`, fileName, uid) .then(resolve); }); }).on('error', (err) => { // Handle errors fs.unlink(datadir + `download/${uid}/${fileName}`); reject(); // Or resolve() if you want to ignore the error and not cause it to prevent the callback from executing later }); // ... }); } 函数,在这里你需要做我们开始做的同样的事情:将进程映射到promises列表。首先,传递的processArray()函数需要返回processgetDataReg()返回的promise:

getDataFtp()

现在,您的// Second param, download and post process (url) => { if(url.startsWith('http')) { return getDataReg(url, uid); } else if(url.startsWith('ftp')) { return getDataFtp(url, uid); } else { console.log('not a valid resource'); } return Promise.reject(); // or Promise.resolve() if you want invalid resources to be ignored and not prevent the callback from executing later } 功能可能如下所示:

processArray()

当所有异步操作都已完成时,将调用您的回调,无论它们执行的顺序如何。如果任何一个承诺拒绝,则回调将永远不会被执行,因此相应地管理您的承诺拒绝。

这是一个带有完整代码的JSFiddle:https://jsfiddle.net/upn4yqsw/

答案 2 :(得分:0)

一般情况下,nodejs似乎没有Streams Standard实现Promise为基础,至少从可以收集的内容开始;但是,使用基于事件或回调的机制,您可以在函数调用中使用Promise构造函数,在调度特定事件时使用return已完成的Promise对象

const doStuff = (...args) => new Promise((resolve, reject)) => {
  /* define and do stream stuff */
  doStreamStuff.on(/* "close", "end" */, => {
    // do stuff
    resolve(/* value */)
  })
});

doStuff(/* args */)
.then(data => {})
.catch(err => {})