Запустите функцию обратного вызова после завершения forEach

1

В проекте у меня есть цикл, проходящий через список URL-адресов. Он загружает файл с каждого URL-адреса и выполняет какой-либо пост-процесс над загруженным файлом.

После всего процесса (как процесса загрузки, так и послепроцесса) я хочу выполнить функцию обратного вызова. Поскольку пост-процесс включает в себя некоторую поточную задачу, он имеет близкое событие. Если последний элемент можно идентифицировать, я могу передать функцию обратного вызова в событие закрытия. Однако, поскольку цикл является асинхронным, я не могу отслеживать, какой элемент выполняется последним.

На данный момент я использую 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) => {});
    });
}
Теги:
callback
foreach
asynchronous

3 ответа

1
Лучший ответ

То, что вы хотите сделать, - сделать все ваши асинхронные процессы сходящимися в одно обещание, которое вы можете использовать для выполнения обратного вызова в нужный момент.

Давайте начнем с точки завершения каждого процесса, который, как я полагаю, находится в unzipData() функции mv() в unzipData(). Вы хотите обернуть каждое из этих асинхронных действий в Promise, которое решает в .forEach() и вы также захотите использовать эти обещания позже, и для этого вы используете метод .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() функцию 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() обещать, возвращаемое 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() к функции processArray() и здесь вам нужно сделать то же самое, что мы сделали для начала: сопоставить процессы в списке обещаний. Во-первых, переданная функция process должна возвращать обещания, возвращаемые getDataReg() и 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() может выглядеть так:

exports.processArray = (items, process, callback) =>
  Promise.all(items.map(process))
    .then(callback)
    .catch(() => console.log('Something went wrong somewhere'));

Обратный вызов будет вызван, когда все асинхронные действия будут завершены независимо от того, в каком порядке они выполняются. Если какое-либо из обещаний отклоняется, обратный вызов никогда не будет выполнен, поэтому соответствующим образом откажитесь от своих обещаний.

Здесь JSFiddle с полным кодом: https://jsfiddle.net/upn4yqsw/

  • 0
    Идеальный ответ! Вы четко поняли и объяснили мой вопрос и нашли отличное решение. Спасибо!
2

После всего процесса (как процесса загрузки, так и послепроцесса) я хочу выполнить функцию обратного вызова.

Интересная вещь о серии асинхронных процессов заключается в том, что вы никогда не узнаете, когда будут завершены все процессы. Поэтому установка тайм-аута для обратного вызова - это быстрый и грязный способ сделать это, но он не является надежным.

Вместо этого вы можете использовать counter для решения этой проблемы. Скажем, у вас есть 10 операций для выполнения. Вначале вы устанавливаете счетчик на counter = 10 После завершения каждого процесса, независимо от того, как (он может либо преуспеть или сработает), вы можете уменьшить счетчик на 1, как counter -= 1 а сразу после него вы можете проверить, счетчик равен 0, если это означает, что все процессы завершены, и мы достигли конца. Теперь вы можете безопасно запускать свою функцию обратного вызова, например if(counter === 0) callback();


Если бы я был вами, я бы сделал что-то вроде этого:

* Обратите внимание, что вызываемый процесс должен вернуть обещание, чтобы я мог знать, когда он закончит (опять же, независимо от того, как)

* Если вам нужна помощь в отношении обещаний, эта полезная статья может вам помочь: https://howtonode.org/promises

* О, и еще одна вещь, вам следует избегать использования arguments.callee, потому что она устарела. Вот почему почему свойство arguments.callee.caller устарело в 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;
      }
    }
};
  • 0
    В моем случае processArray находится в одном файле. Все остальные функции находятся в другом файле. С моей точки зрения, счетчик будет установлен в processArray. Счетчик - = 1 будет выполнен в unzipData. Как я могу справиться с этим?
  • 1
    @zhangjinzhou На самом деле мне не нравятся длинные ответы, и мне не нравятся длинные вопросы, но я расширил свой ответ, чтобы показать вам, как вы можете решить эту проблему. :-)
Показать ещё 1 комментарий
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 => {})
  • 0
    Проблема в том, что я не могу решить, когда выполнить обратный вызов. Это петля, как вы знаете. Это не говорит, когда последний элемент сделан (я считаю, что это точка для выполнения обратного вызова).
  • 0
    Используйте Promise.all() и Array.prototype.map() вместо Array.prototype.forEach() , см. Какой смысл обещаний в JavaScript?
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню