Javascript 完成forEach后运行回调函数

Javascript 完成forEach后运行回调函数,javascript,asynchronous,foreach,callback,Javascript,Asynchronous,Foreach,Callback,在这个项目中,我有一个循环遍历URL列表。它从每个url下载文件,并对下载的文件进行一些后期处理 在完成所有过程(下载过程和后期处理)之后,我想执行一个回调函数。因为后期处理包含一些流式处理任务,所以它有关闭事件。如果可以识别最后一项,我可以将回调函数传递给close事件。但是,由于循环是异步的,所以我无法跟踪最后完成的项目 现在,我使用5秒超时来确保在整个过程之后执行回调。显然,这是不可持续的。处理这个问题的好方法是什么 循环代码: exports.processArray = (items,

在这个项目中,我有一个循环遍历URL列表。它从每个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) => {});
    });
}

一般来说,由于
nodejs
似乎没有实现基于
Promise
的功能,至少从可以收集的信息来看是这样的;但是,使用基于事件或回调机制,您可以在函数调用中使用
Promise
构造函数,在调度特定事件时
返回
已实现的
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 => {})
在完成所有过程(下载过程和后期处理)之后,我想执行一个回调函数

一系列异步进程的有趣之处在于,您永远无法确切知道所有进程何时才能完成。因此,为回调设置超时是一种快速而肮脏的方法,但它并不可靠

您可以使用
计数器来解决此问题。
假设您有10个操作要执行。开始时,您将计数器设置为10
counter=10
并且在每个进程完成后,无论如何(它可能成功或失败),您都可以将计数器减量1,就像
counter-=1
一样,然后您可以检查计数器是否为0,如果是,则意味着所有进程都已完成,我们到达了终点。现在可以安全地运行回调函数,如
if(counter==0)callback()


如果我是你,我会这样做:

*请注意,被调用的进程应该返回一个承诺,这样我就可以知道它何时完成(同样,不管如何)

*如果您需要有关承诺的帮助,这篇有用的文章可能会帮助您:

*还有一件事,您应该避免使用
参数.callee
,因为它已被弃用。这就是原因


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

让我们从每个进程完成的点开始,我假设它在传递给
unzipData()
中的
mv()
函数的回调中。您希望将这些异步操作包装在回调中解析的承诺中,您还希望稍后使用这些承诺,为此,您可以使用
.map()
方法收集数组中的承诺(而不是
.forEach()
)。
代码如下:

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);
接下来,我们需要进一步查看代码。我们可以看到,我们刚才看到的代码本身就是异步操作的事件处理程序的一部分,即
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
    });

    // ...

  });
}
最后,我们进入
processArray()
函数,这里您需要做与开始时相同的事情:将流程映射到承诺列表中。首先,传递的
进程
函数需要返回由
getDataReg()
getDataFtp()返回的承诺:

现在,您的
processArray()
函数可以如下所示:

exports.processArray = (items, process, callback) =>
  Promise.all(items.map(process))
    .then(callback)
    .catch(() => console.log('Something went wrong somewhere'));
当所有异步操作完成时,您的回调将被调用,而不管它们的顺序如何。如果任何一个承诺被拒绝,回调将永远不会执行,因此请相应地管理您的承诺拒绝


下面是一个包含完整代码的JSFIDLE:

通过“正确”处理异步代码来处理它-使用回调或承诺您可以在
processArray()调用中使用
Promise
构造函数。不确定为什么
nodejs
没有实现使用
Promise
而不是回调的流标准。这个问题在过去一周左右出现了好几次。你能用代码示例回答这个问题吗?如果我使用回调,我应该在什么时候执行回调@JaromandaXRelated在我的例子中,processArray位于一个文件中。所有其他函数都在另一个文件中。在我看来,计数器将在processArray中设置。计数器-=1将在unzipData中执行。我该怎么处理呢?@zhangjingzhou实际上我不喜欢长答案,我也不喜欢长问题,但我扩展了我的答案来告诉你,你可以如何解决这个问题。:-)我从不使用诺言。你有什么指示吗?问题是我不能决定什么时候执行回调。正如你所知,这是一个循环。它不会告诉最后一项何时完成(我相信这是执行回调的关键点)。请使用
Promise.all()
Array.prototype.map()
而不是
Array.prototype.forEach()
,请参见“谢谢”。我想Mikael Lennholm用代码来解释它。我想这就是你想要的
// 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
}
exports.processArray = (items, process, callback) =>
  Promise.all(items.map(process))
    .then(callback)
    .catch(() => console.log('Something went wrong somewhere'));