在Node.js中等待多个回调的惯用方法

时间:2011-03-02 19:17:55

标签: node.js

假设您需要执行一些依赖于某些临时文件的操作。以来 我们在这里谈论Node,这些操作显然是异步的。 什么是等待所有操作完成的惯用方法 知道什么时候可以删除临时文件?

以下是一些显示我想要做的代码:

do_something(tmp_file_name, function(err) {});
do_something_other(tmp_file_name, function(err) {});
fs.unlink(tmp_file_name);

但如果我这样写,第三次调用可以在前两次之前执行 有机会使用该文件。我需要一些方法来保证前两个 在没有嵌套的情况下继续调用之前已经完成调用(调用它们的回调) 调用(并使它们在实践中同步)。

我考虑过在回调上使用事件发射器并注册计数器 作为接收者。计数器将收到完成的事件并计算多少 运营仍在等待中。当最后一个完成时,它将删除 文件。但是存在竞争条件的风险,我不确定这是否存在 通常是这样做的。

Node人如何解决这类问题?

8 个答案:

答案 0 :(得分:94)

<强>更新

现在我建议你看一下:

  • Promises

      

    Promise对象用于延迟和异步计算。   承诺代表尚未完成的操作,但是   预计在未来。

    流行的promises库是bluebird。 A建议您查看why promises

      

    你应该使用promises来解决这个问题:

    fs.readFile("file.json", function (err, val) {
        if (err) {
            console.error("unable to read file");
        }
        else {
            try {
                val = JSON.parse(val);
                console.log(val.success);
            }
            catch (e) {
                console.error("invalid json in file");
            }
        }
    });
    
         

    进入这个:

    fs.readFileAsync("file.json").then(JSON.parse).then(function (val) {
        console.log(val.success);
    })
    .catch(SyntaxError, function (e) {
        console.error("invalid json in file");
    })
    .catch(function (e) {
        console.error("unable to read file");
    });
    
  • 生成器:例如通过co

      

    基于生成器的nod​​ejs和浏览器的控制流程良好性,   使用promises,让你以一种很好的方式编写非阻塞代码。

    var co = require('co');
    
    co(function *(){
      // yield any promise
      var result = yield Promise.resolve(true);
    }).catch(onerror);
    
    co(function *(){
      // resolve multiple promises in parallel
      var a = Promise.resolve(1);
      var b = Promise.resolve(2);
      var c = Promise.resolve(3);
      var res = yield [a, b, c];
      console.log(res);
      // => [1, 2, 3]
    }).catch(onerror);
    
    // errors can be try/catched
    co(function *(){
      try {
        yield Promise.reject(new Error('boom'));
      } catch (err) {
        console.error(err.message); // "boom"
     }
    }).catch(onerror);
    
    function onerror(err) {
      // log any uncaught errors
      // co will not throw any errors you do not handle!!!
      // HANDLE ALL YOUR ERRORS!!!
      console.error(err.stack);
    }
    

如果我理解正确,我认为你应该看看非常好的async库。你应该特别看看series。只是来自github页面的片段的副本:

async.series([
    function(callback){
        // do some stuff ...
        callback(null, 'one');
    },
    function(callback){
        // do some more stuff ...
        callback(null, 'two');
    },
],
// optional callback
function(err, results){
    // results is now equal to ['one', 'two']
});


// an example using an object instead of an array
async.series({
    one: function(callback){
        setTimeout(function(){
            callback(null, 1);
        }, 200);
    },
    two: function(callback){
        setTimeout(function(){
            callback(null, 2);
        }, 100);
    },
},
function(err, results) {
    // results is now equals to: {one: 1, two: 2}
});

此外,该库也可以在浏览器中运行。

答案 1 :(得分:22)

最简单的方法是在启动异步操作时递增整数计数器,然后在回调中递减计数器。根据复杂程度,回调可以将计数器检查为零,然后删除文件。

稍微复杂的是维护一个对象列表,每个对象都有你需要识别操作的任何属性(它甚至可以是函数调用)以及状态代码。回调会将状态代码设置为已完成。

然后你会有一个等待的循环(使用process.nextTick)并检查是否所有任务都已完成。这种方法优于计数器的优点是,如果所有未完成的任务都可以完成,则在发出所有任务之前,计数器技术会导致您过早地删除文件。

答案 2 :(得分:11)

// simple countdown latch
function CDL(countdown, completion) {
    this.signal = function() { 
        if(--countdown < 1) completion(); 
    };
}

// usage
var latch = new CDL(10, function() {
    console.log("latch.signal() was called 10 times.");
});

答案 3 :(得分:7)

没有“本机”解决方案,但节点有million flow control libraries。你可能会喜欢Step:

Step(
  function(){
      do_something(tmp_file_name, this.parallel());
      do_something_else(tmp_file_name, this.parallel());
  },
  function(err) {
    if (err) throw err;
    fs.unlink(tmp_file_name);
  }
)

或者,正如迈克尔所说,计数器可能是一个更简单的解决方案。看一下这个semaphore mock-up。你会这样使用它:

do_something1(file, queue('myqueue'));
do_something2(file, queue('myqueue'));

queue.done('myqueue', function(){
  fs.unlink(file);
});

答案 4 :(得分:5)

我想提供另一种解决方案,它利用节点:事件核心的编程范式的速度和效率。

使用事件和简单的状态机可以完成使用Promises或用于管理流控制的模块(如async)所做的一切,我相信这提供了一种方法,可能更容易理解比其他选择。

例如,假设您希望并行处理多个文件的长度:

const EventEmitter = require('events').EventEmitter;

// simple event-driven state machine
const sm = new EventEmitter();

// running state
let context={
  tasks:    0,    // number of total tasks
  active:   0,    // number of active tasks
  results:  []    // task results
};

const next = (result) => { // must be called when each task chain completes

  if(result) { // preserve result of task chain
    context.results.push(result);
  }

  // decrement the number of running tasks
  context.active -= 1; 

  // when all tasks complete, trigger done state
  if(!context.active) { 
    sm.emit('done');
  }
};

// operational states
// start state - initializes context
sm.on('start', (paths) => {
  const len=paths.length;

  console.log(`start: beginning processing of ${len} paths`);

  context.tasks = len;              // total number of tasks
  context.active = len;             // number of active tasks

  sm.emit('forEachPath', paths);    // go to next state
});

// start processing of each path
sm.on('forEachPath', (paths)=>{

  console.log(`forEachPath: starting ${paths.length} process chains`);

  paths.forEach((path) => sm.emit('readPath', path));
});

// read contents from path
sm.on('readPath', (path) => {

  console.log(`  readPath: ${path}`);

  fs.readFile(path,(err,buf) => {
    if(err) {
      sm.emit('error',err);
      return;
    }
    sm.emit('processContent', buf.toString(), path);
  });

});

// compute length of path contents
sm.on('processContent', (str, path) => {

  console.log(`  processContent: ${path}`);

  next(str.length);
});

// when processing is complete
sm.on('done', () => { 
  const total = context.results.reduce((sum,n) => sum + n);
  console.log(`The total of ${context.tasks} files is ${total}`);
});

// error state
sm.on('error', (err) => { throw err; });

// ======================================================
// start processing - ok, let's go
// ======================================================
sm.emit('start', ['file1','file2','file3','file4']);

将输出:

start: beginning processing of 4 paths
forEachPath: starting 4 process chains
  readPath: file1
  readPath: file2
  processContent: file1
  readPath: file3
  processContent: file2
  processContent: file3
  readPath: file4
  processContent: file4
The total of 4 files is 4021

请注意,流程链任务的顺序取决于系统负载。

您可以将程序流设想为:

start -> forEachPath -+-> readPath1 -> processContent1 -+-> done
                      +-> readFile2 -> processContent2 -+
                      +-> readFile3 -> processContent3 -+
                      +-> readFile4 -> processContent4 -+

为了重复使用,创建一个支持各种流控制模式的模块是微不足道的,即串行,并行,批处理,直到等等。

答案 5 :(得分:2)

最简单的解决方案是按顺序运行do_something *和unlink,如下所示:

do_something(tmp_file_name, function(err) {
    do_something_other(tmp_file_name, function(err) {
        fs.unlink(tmp_file_name);
    });
});

除非出于性能原因,你想并行执行do_something()和do_something_other(),否则我建议保持简单并按照这种方式执行。

答案 6 :(得分:1)

等待https://github.com/luciotato/waitfor

使用Wait.for:

var wait=require('wait.for');

...in a fiber...

wait.for(do_something,tmp_file_name);
wait.for(do_something_other,tmp_file_name);
fs.unlink(tmp_file_name);

答案 7 :(得分:0)

使用纯Promises可能会有些混乱,但是如果您使用Deferred Promises,那还不算太糟:

安装:

npm install --save @bitbar/deferred-promise

修改您的代码:

const DeferredPromise = require('@bitbar/deferred-promise');

const promises = [
  new DeferredPromise(),
  new DeferredPromise()
];

do_something(tmp_file_name, (err) => {
  if (err) {
    promises[0].reject(err);
  } else {
    promises[0].resolve();
  }
});

do_something_other(tmp_file_name, (err) => {
  if (err) {
    promises[1].reject(err);
  } else {
    promises[1].resolve();
  }
});

Promise.all(promises).then( () => {
  fs.unlink(tmp_file_name);
});