Cómo evitar el anidamiento prolongado de funciones asincrónicas en Node.js

158

Quiero hacer una página que muestre algunos datos de una base de datos, por lo que he creado algunas funciones que obtienen esos datos de mi base de datos. Solo soy un novato en Node.js, por lo que entiendo, si quiero usarlos todos en una sola página (respuesta HTTP) tendría que anidarlos a todos:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

Si hay muchas funciones como esa, entonces el anidamiento se convierte en un problema .

Hay alguna manera de evitar esto? Supongo que tiene que ver con cómo combinas múltiples funciones asincrónicas, lo que parece ser algo fundamental.

Kay pálido
fuente
12
Entonces, cuando tienes 10 funciones asíncronas, ¿tienes 10 niveles de sangría?
Kay Pale el
Este enlace puede ayudar. stackoverflow.com/a/4631909/290340
Evan Plaice
1
Otro problema: insertar otra función entre getSomeDatey getSomeOtherDatetermina cambiando la sangría de muchas líneas, lo que hace que el historial de git sea más difícil de leer ( git blameincluso es inútil después de esto), y es probable que cometas errores al hacerlo manualmente
Daniel Alder

Respuestas:

73

Interesante observación. Tenga en cuenta que en JavaScript normalmente puede reemplazar las funciones de devolución de llamada anónimas en línea con variables de función con nombre.

El seguimiento:

http.createServer(function (req, res) {
   // inline callback function ...

   getSomeData(client, function (someData) {
      // another inline callback function ...

      getMoreData(client, function(moreData) {
         // one more inline callback function ...
      });
   });

   // etc ...
});

Podría reescribirse para parecerse a esto:

var moreDataParser = function (moreData) {
   // date parsing logic
};

var someDataParser = function (someData) {
   // some data parsing logic

   getMoreData(client, moreDataParser);
};

var createServerCallback = function (req, res) {
   // create server logic

   getSomeData(client, someDataParser);

   // etc ...
};

http.createServer(createServerCallback);

Sin embargo, a menos que planee reutilizar la lógica de devolución de llamada en otros lugares, a menudo es mucho más fácil leer funciones anónimas en línea, como en su ejemplo. También le ahorrará tener que encontrar un nombre para todas las devoluciones de llamada.

Además, tenga en cuenta que, como señaló @pst en un comentario a continuación, si está accediendo a las variables de cierre dentro de las funciones internas, lo anterior no sería una traducción directa. En tales casos, usar funciones anónimas en línea es aún más preferible.

Daniel Vassallo
fuente
26
Sin embargo, (y realmente solo para entender la compensación) cuando no está anidado, se puede perder cierta semántica de cierre sobre las variables , por lo que no es una traducción directa. En el ejemplo anterior, getMoreDatase pierde el acceso a 'res' en .
2
Creo que su solución está rota: en someDataParserrealidad analiza TODOS los datos, ya que también llama getMoreData. En ese sentido, el nombre de la función es incorrecto y se hace evidente que en realidad no hemos eliminado el problema de anidamiento.
Konstantin Schubert
63

Kay, simplemente usa uno de estos módulos.

Se convertirá esto:

dbGet('userIdOf:bobvance', function(userId) {
    dbSet('user:' + userId + ':email', '[email protected]', function() {
        dbSet('user:' + userId + ':firstName', 'Bob', function() {
            dbSet('user:' + userId + ':lastName', 'Vance', function() {
                okWeAreDone();
            });
        });
    });
});

Dentro de esto:

flow.exec(
    function() {
        dbGet('userIdOf:bobvance', this);

    },function(userId) {
        dbSet('user:' + userId + ':email', '[email protected]', this.MULTI());
        dbSet('user:' + userId + ':firstName', 'Bob', this.MULTI());
        dbSet('user:' + userId + ':lastName', 'Vance', this.MULTI());

    },function() {
        okWeAreDone()
    }
);
Baggz
fuente
9
Eché un vistazo rápido a flow-js, step y async y parece que solo tratan con el orden de ejecución de la función. En mi caso, hay acceso a variables de cierre en línea en cada sangría. Entonces, por ejemplo, las funciones funcionan así: obtener requisitos / res HTTP, obtener ID de usuario de DB para cookie, obtener correo electrónico para el ID de usuario posterior, obtener más datos para el correo electrónico posterior, ..., obtener X para más adelante Y, ... Si no me equivoco, estos marcos solo aseguran que las funciones asíncronas se ejecutarán en el orden correcto, pero en cada cuerpo de función no hay forma de obtener la variable proporcionada naturalmente por los cierres (?) Gracias :)
Kay Pale
9
En términos de clasificación de estas bibliotecas, verifiqué el número de "Estrellas" en cada una de ellas en Github. async tiene la mayor cantidad con aproximadamente 3000, el paso es el siguiente con aproximadamente 1000, los otros son significativamente menos. Por supuesto, no todos hacen lo mismo :-)
kgilpin
3
@KayPale Tiendo a usar async.waterfall, y algunas veces tendré mis propias funciones para cada etapa / paso que transmitirá lo que necesita el siguiente paso o definirá variables antes de la llamada async.METHOD para que esté disponible en la línea descendente. También usaré METHODNAME.bind (...) para mis llamadas async. *, Que también funciona bastante bien.
Rastreador1
Una pregunta rápida: en su lista de módulos, ¿son los dos últimos iguales? Es decir, "async.js" y "async"
dari0h
18

En su mayor parte, estaría de acuerdo con Daniel Vassallo. Si puede dividir una función complicada y profundamente anidada en funciones con nombre separadas, entonces esa es generalmente una buena idea. Para los momentos en que tiene sentido hacerlo dentro de una sola función, puede usar una de las muchas bibliotecas asíncronas de node.js disponibles. Las personas han ideado muchas formas diferentes de abordar esto, así que eche un vistazo a la página de módulos de node.js y vea lo que piensa.

Yo mismo escribí un módulo para esto, llamado async.js . Con esto, el ejemplo anterior podría actualizarse a:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  async.series({
    someData: async.apply(getSomeDate, client),
    someOtherData: async.apply(getSomeOtherDate, client),
    moreData: async.apply(getMoreData, client)
  },
  function (err, results) {
    var html = "<h1>Demo page</h1>";
    html += "<p>" + results.someData + "</p>";
    html += "<p>" + results.someOtherData + "</p>";
    html += "<p>" + results.moreData + "</p>";
    res.write(html);
    res.end();
  });
});

Una cosa buena de este enfoque es que puede cambiar rápidamente su código para obtener los datos en paralelo cambiando la función 'serie' a 'paralelo'. Además, async.js también funcionará dentro del navegador, por lo que puede usar los mismos métodos que usaría en node.js si encuentra algún código asincrónico complicado.

Espero que sea útil!

Caolan
fuente
Hola Caolan y gracias por la respuesta! En mi caso, hay acceso a variables de cierre en línea en cada sangría. Entonces, por ejemplo, las funciones funcionan así: obtener requisitos / res HTTP, obtener ID de usuario de DB para cookie, obtener correo electrónico para el ID de usuario posterior, obtener más datos para el correo electrónico posterior, ..., obtener X para más adelante Y, ... Si no me equivoco, el código que sugiere solo asegura que las funciones asíncronas se ejecutarán en el orden correcto, pero en cada cuerpo de función no hay forma de obtener la variable proporcionada naturalmente por los cierres en mi código original. ¿Es ese el caso?
Kay Pale
3
Lo que está tratando de lograr se llama arquitectónicamente una tubería de datos. Puede usar la cascada asíncrona para tales casos.
Rudolf Meijering
18

Podría usar este truco con una matriz en lugar de funciones anidadas o un módulo.

Mucho más fácil para los ojos.

var fs = require("fs");
var chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step3");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step4");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("step5");
        fs.stat("f2.js",chain.shift());
    },
    function(err, stats) {
        console.log("done");
    },
];
chain.shift()();

Puede ampliar el idioma para procesos paralelos o incluso cadenas paralelas de procesos:

var fs = require("fs");
var fork1 = 2, fork2 = 2, chain = [
    function() { 
        console.log("step1");
        fs.stat("f1.js",chain.shift());
    },
    function(err, stats) {
        console.log("step2");
        var next = chain.shift();
        fs.stat("f2a.js",next);
        fs.stat("f2b.js",next);
    },
    function(err, stats) {
        if ( --fork1 )
            return;
        console.log("step3");
        var next = chain.shift();

        var chain1 = [
            function() { 
                console.log("step4aa");
                fs.stat("f1.js",chain1.shift());
            },
            function(err, stats) { 
                console.log("step4ab");
                fs.stat("f1ab.js",next);
            },
        ];
        chain1.shift()();

        var chain2 = [
            function() { 
                console.log("step4ba");
                fs.stat("f1.js",chain2.shift());
            },
            function(err, stats) { 
                console.log("step4bb");
                fs.stat("f1ab.js",next);
            },
        ];
        chain2.shift()();
    },
    function(err, stats) {
        if ( --fork2 )
            return;
        console.log("done");
    },
];
chain.shift()();
Guido
fuente
15

Me gusta mucho async.js para este propósito.

El problema se resuelve con el comando en cascada:

cascada (tareas, [devolución de llamada])

Ejecuta una serie de funciones en serie, cada una pasando sus resultados a la siguiente en la matriz. Sin embargo, si alguna de las funciones pasa un error a la devolución de llamada, la siguiente función no se ejecuta y la devolución de llamada principal se llama inmediatamente con el error.

Argumentos

tareas: una matriz de funciones para ejecutar, cada función recibe una devolución de llamada (err, result1, result2, ...) que debe llamar al finalizar. El primer argumento es un error (que puede ser nulo) y cualquier argumento adicional se pasará como argumento para la siguiente tarea. callback (err, [results]) - Una devolución de llamada opcional para ejecutarse una vez que se hayan completado todas las funciones. Esto pasará los resultados de la devolución de llamada de la última tarea.

Ejemplo

async.waterfall([
    function(callback){
        callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
        callback(null, 'three');
    },
    function(arg1, callback){
        // arg1 now equals 'three'
        callback(null, 'done');
    }
], function (err, result) {
    // result now equals 'done'    
});

En cuanto a las variables req, res, se compartirán dentro del mismo alcance que la función (req, res) {} que encierra toda la llamada async.waterfall.

No solo eso, async es muy limpio. Lo que quiero decir es que cambio muchos casos como este:

function(o,cb){
    function2(o,function(err, resp){
        cb(err,resp);
    })
}

Primero:

function(o,cb){
    function2(o,cb);
}

Entonces a esto:

function2(o,cb);

Entonces a esto:

async.waterfall([function2,function3,function4],optionalcb)

También permite que muchas funciones prefabricadas preparadas para async se invoquen desde util.js muy rápido. Simplemente encadene lo que quiere hacer, asegúrese de que cb se maneje universalmente. Esto acelera mucho todo el proceso de codificación.

Grant Li
fuente
11

Lo que necesitas es un poco de azúcar sintáctica. Mira esto:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = ["<h1>Demo page</h1>"];
  var pushHTML = html.push.bind(html);

  Queue.push( getSomeData.partial(client, pushHTML) );
  Queue.push( getSomeOtherData.partial(client, pushHTML) );
  Queue.push( getMoreData.partial(client, pushHTML) );
  Queue.push( function() {
    res.write(html.join(''));
    res.end();
  });
  Queue.execute();
}); 

Bastante ordenado , ¿no? Puede notar que html se convirtió en una matriz. Esto se debe en parte a que las cadenas son inmutables, por lo que es mejor que proteja su salida en una matriz, que descartar cadenas cada vez más grandes. La otra razón es debido a otra buena sintaxis con bind.

Queueen el ejemplo es realmente solo un ejemplo y junto con partialse puede implementar de la siguiente manera

// Functional programming for the rescue
Function.prototype.partial = function() {
  var fun = this,
      preArgs = Array.prototype.slice.call(arguments);
  return function() {
    fun.apply(null, preArgs.concat.apply(preArgs, arguments));
  };
};

Queue = [];
Queue.execute = function () {
  if (Queue.length) {
    Queue.shift()(Queue.execute);
  }
};
gblazex
fuente
1
Queue.execute () simplemente ejecutará los parciales uno tras otro, sin esperar los resultados de las llamadas asíncronas.
ngn
En el clavo, gracias. He actualizado la respuesta. Aquí hay una prueba: jsbin.com/ebobo5/edit (con una lastfunción opcional )
gblazex
Hola galambalazs y gracias por la respuesta! En mi caso, hay acceso a variables de cierre en línea en cada sangría. Entonces, por ejemplo, las funciones funcionan así: obtener requisitos / res HTTP, obtener ID de usuario de DB para cookie, obtener correo electrónico para el ID de usuario posterior, obtener más datos para el correo electrónico posterior, ..., obtener X para más adelante Y, ... Si no me equivoco, el código que sugiere solo asegura que las funciones asíncronas se ejecutarán en el orden correcto, pero en cada cuerpo de función no hay forma de obtener la variable proporcionada naturalmente por los cierres en mi código original. ¿Es ese el caso?
Kay Pale
1
Bueno, definitivamente pierdes cierres en todas las respuestas. Lo que puede hacer es crear un objeto en el ámbito global para datos compartidos . Entonces, por ejemplo, su primera función agrega obj.emaily su próxima función usa y obj.emailluego la elimina (o simplemente asigna null).
gblazex
7

Estoy enamorado de Async.js desde que lo encontré. Tiene una async.seriesfunción que puede usar para evitar anidamientos largos.

Documentación:-


series (tareas, [devolución de llamada])

Ejecute una serie de funciones en serie, cada una ejecutándose una vez que se haya completado la función anterior. [...]

Argumentos

tasks- Una serie de funciones para ejecutar, cada función recibe una devolución de llamada que debe llamar al finalizar. callback(err, [results])- Una devolución de llamada opcional para ejecutarse una vez que se hayan completado todas las funciones. Esta función obtiene una matriz de todos los argumentos pasados ​​a las devoluciones de llamada utilizadas en la matriz.


Así es como podemos aplicarlo a su código de ejemplo: -

http.createServer(function (req, res) {

    res.writeHead(200, {'Content-Type': 'text/html'});

    var html = "<h1>Demo page</h1>";

    async.series([
        function (callback) {
            getSomeData(client, function (someData) { 
                html += "<p>"+ someData +"</p>";

                callback();
            });
        },

        function (callback) {
            getSomeOtherData(client, function (someOtherData) { 
                html += "<p>"+ someOtherData +"</p>";

                callback(); 
            });
        },

        funciton (callback) {
            getMoreData(client, function (moreData) {
                html += "<p>"+ moreData +"</p>";

                callback();
            });
        }
    ], function () {
        res.write(html);
        res.end();
    });
});
Salman von Abbas
fuente
6

El azúcar sintáctico más simple que he visto es la promesa de nodo.

npm install node-promise || git clone https://github.com/kriszyp/node-promise

Con esto, puede encadenar métodos asíncronos como:

firstMethod().then(secondMethod).then(thirdMethod);

El valor de retorno de cada uno está disponible como argumento en el siguiente.

Nikhil Ranjan
fuente
3

Lo que ha hecho allí es tomar un patrón asíncrono y aplicarlo a 3 funciones llamadas en secuencia, cada una esperando que se complete la anterior antes de comenzar, es decir, las ha sincronizado . El punto sobre la programación asíncrona es que puede tener varias funciones ejecutándose todas a la vez y no tener que esperar a que se complete cada una.

si getSomeDate () no proporciona nada para getSomeOtherDate (), que no proporciona nada para getMoreData (), ¿por qué no los llama de forma asincrónica como permite js o si son interdependientes (y no asincrónicos) escríbalos como función única?

No necesita usar el anidamiento para controlar el flujo; por ejemplo, haga que cada función termine llamando a una función común que determina cuándo se han completado los 3 y luego envía la respuesta.

Nick Tulett
fuente
2

Supongamos que pudieras hacer esto:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});
    var html = "<h1>Demo page</h1>";
    chain([
        function (next) {
            getSomeDate(client, next);
        },
        function (next, someData) {
            html += "<p>"+ someData +"</p>";
            getSomeOtherDate(client, next);
        },
        function (next, someOtherData) {
            html += "<p>"+ someOtherData +"</p>";
            getMoreData(client, next);
        },
        function (next, moreData) {
            html += "<p>"+ moreData +"</p>";
            res.write(html);
            res.end();
        }
    ]);
});

Solo necesita implementar chain () para que aplique parcialmente cada función a la siguiente, e inmediatamente invoca solo la primera función:

function chain(fs) {
    var f = function () {};
    for (var i = fs.length - 1; i >= 0; i--) {
        f = fs[i].partial(f);
    }
    f();
}
ngn
fuente
Hola ngn y gracias por la respuesta! En mi caso, hay acceso a variables de cierre en línea en cada sangría. Entonces, por ejemplo, las funciones funcionan así: obtener requisitos / res HTTP, obtener ID de usuario de DB para cookie, obtener correo electrónico para el ID de usuario posterior, obtener más datos para el correo electrónico posterior, ..., obtener X para más adelante Y, ... Si no me equivoco, el código que sugiere solo asegura que las funciones asíncronas se ejecutarán en el orden correcto, pero en cada cuerpo de función no hay forma de obtener la variable proporcionada naturalmente por los cierres en mi código original. ¿Es ese el caso?
Kay Pale
2

el infierno de devolución de llamada se puede evitar fácilmente en JavaScript puro con cierre. La solución a continuación supone que todas las devoluciones de llamada siguen la firma de la función (error, datos).

http.createServer(function (req, res) {
  var modeNext, onNext;

  // closure variable to keep track of next-callback-state
  modeNext = 0;

  // next-callback-handler
  onNext = function (error, data) {
    if (error) {
      modeNext = Infinity;
    } else {
      modeNext += 1;
    }
    switch (modeNext) {

    case 0:
      res.writeHead(200, {'Content-Type': 'text/html'});
      var html = "<h1>Demo page</h1>";
      getSomeDate(client, onNext);
      break;

    // handle someData
    case 1:
        html += "<p>"+ data +"</p>";
        getSomeOtherDate(client, onNext);
        break;

    // handle someOtherData
    case 2:
      html += "<p>"+ data +"</p>";
      getMoreData(client, onNext);
      break;

    // handle moreData
    case 3:
      html += "<p>"+ data +"</p>";
      res.write(html);
      res.end();
      break;

    // general catch-all error-handler
    default:
      res.statusCode = 500;
      res.end(error.message + '\n' + error.stack);
    }
  };
  onNext();
});
kai zhu
fuente
1

Recientemente he creado una abstracción más simple llamada wait.for para llamar a funciones asíncronas en modo de sincronización (basado en Fibras). Está en una etapa temprana pero funciona. Es en:

https://github.com/luciotato/waitfor

Usando wait.for , puede llamar a cualquier función asíncrona de nodejs estándar, como si fuera una función de sincronización.

usando wait.for su código podría ser:

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

http.createServer(function(req, res) {
  wait.launchFiber(handleRequest,req, res); //run in a Fiber, keep node spinning
}).listen(8080);


//in a fiber
function handleRequest(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  var someData = wait.for(getSomeDate,client);
  html += "<p>"+ someData +"</p>";
  var someOtherData = wait.for(getSomeOtherDate,client);
  html += "<p>"+ someOtherData +"</p>";
  var moreData = wait.for(getMoreData,client);
  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
};

... o si quieres ser menos detallado (y también agregar errores de captura)

//in a fiber
function handleRequest(req, res) {
  try {
    res.writeHead(200, {'Content-Type': 'text/html'});
    res.write(
    "<h1>Demo page</h1>" 
    + "<p>"+ wait.for(getSomeDate,client) +"</p>"
    + "<p>"+ wait.for(getSomeOtherDate,client) +"</p>"
    + "<p>"+ wait.for(getMoreData,client) +"</p>"
    );
    res.end();
  }
  catch(err) {
   res.end('error '+e.message); 
  }

};

En todos los casos, getSomeDate , getSomeOtherDate y getMoreData deben ser funciones asíncronas estándar con el último parámetro una devolución de llamada de función (err, data)

como en:

function getMoreData(client, callback){
  db.execute('select moredata from thedata where client_id=?',[client.id],
       ,function(err,data){
          if (err) callback(err);
          callback (null,data);
        });
}
Lucio M. Tato
fuente
1

Para resolver este problema, escribí nodent ( https://npmjs.org/package/nodent ) que preprocesa invisiblemente su JS. Su código de ejemplo sería (asíncrono, realmente, lea los documentos).

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  someData <<= getSomeDate(client) ;

  html += "<p>"+ someData +"</p>";
  someOtherData <<= getSomeOtherDate(client) ;

  html += "<p>"+ someOtherData +"</p>";
  moreData <<= getMoreData(client) ;

  html += "<p>"+ moreData +"</p>";
  res.write(html);
  res.end();
});

Claramente, hay muchas otras soluciones, pero el preprocesamiento tiene la ventaja de tener poca o ninguna sobrecarga de tiempo de ejecución y, gracias al soporte del mapa de origen, también es fácil de depurar.

MatAtBread
fuente
0

Yo tuve el mismo problema. He visto las principales bibliotecas para ejecutar funciones asíncronas de nodo, y presentan un encadenamiento no natural (debe usar tres o más métodos confs, etc.) para construir su código.

Pasé algunas semanas desarrollando una solución simple y fácil de leer. Por favor, intente con EnqJS . Todas las opiniones serán apreciadas.

En vez de:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";
  getSomeDate(client, function(someData) {
    html += "<p>"+ someData +"</p>";
    getSomeOtherDate(client, function(someOtherData) {
      html += "<p>"+ someOtherData +"</p>";
      getMoreData(client, function(moreData) {
        html += "<p>"+ moreData +"</p>";
        res.write(html);
        res.end();
      });
    });
  });

con EnqJS:

http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  var html = "<h1>Demo page</h1>";

  enq(function(){
    var self=this;
    getSomeDate(client, function(someData){
      html += "<p>"+ someData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getSomeOtherDate(client, function(someOtherData){ 
      html += "<p>"+ someOtherData +"</p>";
      self.return();
    })
  })(function(){
    var self=this;
    getMoreData(client, function(moreData) {
      html += "<p>"+ moreData +"</p>";
      self.return();
      res.write(html);
      res.end();
    });
  });
});

Observe que el código parece ser más grande que antes. Pero no está anidado como antes. Para parecer más natural, las cadenas se llaman inmediatamente:

enq(fn1)(fn2)(fn3)(fn4)(fn4)(...)

Y para decir que regresó, dentro de la función que llamamos:

this.return(response)
Thadeu de Paula
fuente
0

Lo hago de una manera bastante primitiva pero efectiva. Por ejemplo, necesito obtener un modelo con sus padres e hijos y digamos que necesito hacer consultas separadas para ellos:

var getWithParents = function(id, next) {
  var getChildren = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      },
      getParents = function(model, next) {
        /*... code ... */
        return next.pop()(model, next);
      }
      getModel = function(id, next) {
        /*... code ... */
        if (model) {
          // return next callbacl
          return next.pop()(model, next);
        } else {
          // return last callback
          return next.shift()(null, next);
        }
      }

  return getModel(id, [getParents, getChildren, next]);
}
mvbl fst
fuente
0

Use Fibras https://github.com/laverdet/node-fibers hace que el código asincrónico se vea como sincrónico (sin bloqueo)

Yo personalmente uso este pequeño contenedor http://alexeypetrushin.github.com/synchronize Muestra de código de mi proyecto (cada método es realmente asíncrono, trabajando con un archivo asincrónico IO) Incluso tengo miedo de imaginar qué desastre sería con la devolución de llamada o bibliotecas auxiliares de control de flujo asíncrono.

_update: (version, changesBasePath, changes, oldSite) ->
  @log 'updating...'
  @_updateIndex version, changes
  @_updateFiles version, changesBasePath, changes
  @_updateFilesIndexes version, changes
  configChanged = @_updateConfig version, changes
  @_updateModules version, changes, oldSite, configChanged
  @_saveIndex version
  @log "updated to #{version} version"
Alexey Petrushin
fuente
0

Task.js te ofrece esto:

spawn(function*() {
    try {
        var [foo, bar] = yield join(read("foo.json"),
                                    read("bar.json")).timeout(1000);
        render(foo);
        render(bar);
    } catch (e) {
        console.log("read failed: " + e);
    }
});

En lugar de esto:

var foo, bar;
var tid = setTimeout(function() { failure(new Error("timed out")) }, 1000);

var xhr1 = makeXHR("foo.json",
                   function(txt) { foo = txt; success() },
                   function(err) { failure() });
var xhr2 = makeXHR("bar.json",
                   function(txt) { bar = txt; success() },
                   function(e) { failure(e) });

function success() {
    if (typeof foo === "string" && typeof bar === "string") {
        cancelTimeout(tid);
        xhr1 = xhr2 = null;
        render(foo);
        render(bar);
    }
}

function failure(e) {
    xhr1 && xhr1.abort();
    xhr1 = null;
    xhr2 && xhr2.abort();
    xhr2 = null;
    console.log("read failed: " + e);
}
Janus Troelsen
fuente
0

Después de que los demás respondieron, usted declaró que su problema eran variables locales. Parece que una manera fácil de hacer esto es escribir una función externa para contener esas variables locales, luego usar un grupo de funciones internas con nombre y acceder a ellas por nombre. De esta manera, solo anidará dos de profundidad, independientemente de cuántas funciones necesite encadenar.

Aquí está el intento de mi novato de usar el mysqlmódulo Node.js con anidamiento:

function with_connection(sql, bindings, cb) {
    pool.getConnection(function(err, conn) {
        if (err) {
            console.log("Error in with_connection (getConnection): " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, function(err, results) {
            if (err) {
                console.log("Error in with_connection (query): " + JSON.stringify(err));
                cb(true);
                return;
            }
            console.log("with_connection results: " + JSON.stringify(results));
            cb(false, results);
        });
    });
}

La siguiente es una reescritura utilizando funciones internas con nombre. La función externa también with_connectionse puede utilizar como soporte para variables locales. (Aquí, tengo los parámetros sql, bindings, cbque actúan de una manera similar, pero sólo se puede definir algunas variables locales adicionales en with_connection.)

function with_connection(sql, bindings, cb) {

    function getConnectionCb(err, conn) {
        if (err) {
            console.log("Error in with_connection/getConnectionCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        conn.query(sql, bindings, queryCb);
    }

    function queryCb(err, results) {
        if (err) {
            console.log("Error in with_connection/queryCb: " + JSON.stringify(err));
            cb(true);
            return;
        }
        cb(false, results);
    }

    pool.getConnection(getConnectionCb);
}

Había estado pensando que quizás sería posible hacer un objeto con variables de instancia, y usar estas variables de instancia como reemplazo de las variables locales. Pero ahora encuentro que el enfoque anterior que usa funciones anidadas y variables locales es más simple y más fácil de entender. Se necesita un tiempo para desaprender OO, parece :-)

Así que aquí está mi versión anterior con un objeto y variables de instancia.

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, function(err, results) { self.query(err, results); });
}
DbConnection.prototype.query = function(err, results) {
    var self = this;
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        self.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    self.cb(false, results);
}

function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    pool.getConnection(function (err, conn) { dbc.getConnection(err, conn); });
}

Resulta que bindse puede utilizar con alguna ventaja. Me permite deshacerme de las funciones anónimas algo feas que he creado que no hicieron mucho, excepto reenviarme a una llamada al método. No pude pasar el método directamente porque habría estado involucrado con el valor incorrecto de this. Pero con bind, puedo especificar el valor thisque quiero.

function DbConnection(sql, bindings, cb) {
    this.sql = sql;
    this.bindings = bindings;
    this.cb = cb;
}
DbConnection.prototype.getConnection = function(err, conn) {
    var f = this.query.bind(this);
    if (err) {
        console.log("Error in DbConnection.getConnection: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    conn.query(this.sql, this.bindings, f);
}
DbConnection.prototype.query = function(err, results) {
    if (err) {
        console.log("Error in DbConnection.query: " + JSON.stringify(err));
        this.cb(true);
        return;
    }
    console.log("DbConnection results: " + JSON.stringify(results));
    this.cb(false, results);
}

// Get a connection from the pool, execute `sql` in it
// with the given `bindings`.  Invoke `cb(true)` on error,
// invoke `cb(false, results)` on success.  Here,
// `results` is an array of results from the query.
function with_connection(sql, bindings, cb) {
    var dbc = new DbConnection(sql, bindings, cb);
    var f = dbc.getConnection.bind(dbc);
    pool.getConnection(f);
}

Por supuesto, nada de esto es JS adecuado con la codificación Node.js, solo pasé un par de horas en ello. ¿Pero tal vez con un poco de pulido esta técnica puede ayudar?

hibbelig
fuente
0

Usando cable su código se vería así:

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/html'});

    var l = new Wire();

    getSomeDate(client, l.branch('someData'));
    getSomeOtherDate(client, l.branch('someOtherData'));
    getMoreData(client, l.branch('moreData'));

    l.success(function(r) {
        res.write("<h1>Demo page</h1>"+
            "<p>"+ r['someData'] +"</p>"+
            "<p>"+ r['someOtherData'] +"</p>"+
            "<p>"+ r['moreData'] +"</p>");
        res.end();
    });
});
Daniel Garmoshka
fuente
0

para su conocimiento considere Jazz.js https://github.com/Javanile/Jazz.js/wiki/Script-showcase

    const jj = require ('jazz.js');

    // pila ultracompatible
    jj.script ([
        a => ProcessTaskOneCallbackAtEnd (a),
        b => ProcessTaskTwoCallbackAtEnd (b),
        c => ProcessTaskThreeCallbackAtEnd (c),
        d => ProcessTaskFourCallbackAtEnd (d),
        e => ProcessTaskFiveCallbackAtEnd (e),
    ]);

cicciodarkast
fuente