2012-05-24 22 views
8

Mire el fragmento de código siguiente. Tengo una matriz de objetos JSON llamada 'stuObjList'. Quiero hacer un bucle a través de la matriz para encontrar objetos JSON específicos con un determinado conjunto de banderas, y luego hacer una llamada db para recuperar más datos.en nodejs, cómo detener un bucle FOR hasta que la llamada mongodb devuelva

Por supuesto, el bucle FOR no espera a que la llamada db regrese y alcanza el final con j == length. Y cuando la llamada db retorna, el índice 'j' está más allá del índice de matriz. Entiendo cómo funciona node.js y este es el comportamiento esperado.

Mi pregunta es, ¿cuál es el trabajo por aquí. ¿Cómo puedo lograr lo que intento lograr? Gracias, --su

............... 
............... 
............... 
else 
{ 
    console.log("stuObjList.length: " + stuObjList.length); 
    var j = 0; 
    for(j = 0; j < stuObjList.length; j++) 
    { 
    if(stuObjList[j]['honor_student'] != null) 
    {  
     db.collection("students").findOne({'_id' : stuObjList[j]['_id'];}, function(err, origStuObj) 
     { 
     var marker = stuObjList[j]['_id']; 
     var major = stuObjList[j]['major']; 
     }); 
    } 

    if(j == stuObjList.length) 
    { 
     process.nextTick(function() 
     { 
     callback(stuObjList); 
     }); 
    } 
    } 
} 
}); 

Respuesta

8

"async" es un módulo muy popular para abstraerse de bucle asíncrono y hacer que su código sea más fácil de leer/mantener. Por ejemplo:

var async = require('async'); 

function getHonorStudentsFrom(stuObjList, callback) { 

    var honorStudents = []; 

    // The 'async.forEach()' function will call 'iteratorFcn' for each element in 
    // stuObjList, passing a student object as the first param and a callback 
    // function as the second param. Run the callback to indicate that you're 
    // done working with the current student object. Anything you pass to done() 
    // is interpreted as an error. In that scenario, the iterating will stop and 
    // the error will be passed to the 'doneIteratingFcn' function defined below. 
    var iteratorFcn = function(stuObj, done) { 

     // If the current student object doesn't have the 'honor_student' property 
     // then move on to the next iteration. 
     if(!stuObj.honor_student) { 
      done(); 
      return; // The return statement ensures that no further code in this 
        // function is executed after the call to done(). This allows 
        // us to avoid writing an 'else' block. 
     } 

     db.collection("students").findOne({'_id' : stuObj._id}, function(err, honorStudent) 
     { 
      if(err) { 
       done(err); 
       return; 
      } 

      honorStudents.push(honorStudent); 
      done(); 
      return; 
     }); 
    }; 

    var doneIteratingFcn = function(err) { 
     // In your 'callback' implementation, check to see if err is null/undefined 
     // to know if something went wrong. 
     callback(err, honorStudents); 
    }; 

    // iteratorFcn will be called for each element in stuObjList. 
    async.forEach(stuObjList, iteratorFcn, doneIteratingFcn); 
} 

por lo que podría utilizar de esta manera:

getHonorStudentsFrom(studentObjs, function(err, honorStudents) { 
    if(err) { 
     // Handle the error 
     return; 
    } 

    // Do something with honroStudents 
}); 

Tenga en cuenta que .forEach() llamará a su función de repetidor para cada elemento de stuObjList "en paralelo" (es decir, que no va a esperar para que una función de iterador termine siendo llamada para un elemento de matriz antes de llamarlo al siguiente elemento de matriz). Esto significa que no se puede predecir el orden en que funciona el iterador, o más importante, las llamadas a la base de datos. Resultado final: estudiantes de orden de honor impredecibles. Si el orden es importante, use la función .forEachSeries().

+0

@Clint ... muchas gracias. Voy a probar esto y dejarte saber cómo funciona. –

+0

@Clint ... eso funcionó. ¡muchas gracias! –

1

Ah la belleza y la frustración de pensar asincrónicamente. Prueba esto:

............... 
............... 
............... 
else 
{ 
    console.log("stuObjList.length: " + stuObjList.length); 
    var j = 0, found = false, step; 
    for(j = 0; j < stuObjList.length; j++) 
    { 
    if(stuObjList[j]['honor_student'] != null) 
    {  
     found = true; 
     step = j; 
     db.collection("students").findOne({'_id' : stuObjList[j]['_id'];}, function(err, origStuObj) 
     { 
     var marker = stuObjList[step]['_id']; // because j's loop has moved on 
     var major = stuObjList[step]['major']; 
     process.nextTick(function() 
     { 
      callback(stuObjList); 
     }); 
     }); 
    } 

    } 
    if (!found) { 
    process.nextTick(function() 
    { 
     callback(stuObjList); 
    }); 
    } 
} 
}); 

Si encuentra que su "cuando he terminado" pasos se complican, extracto de ellas a otra función, y acaba de llamar desde cada punto. En este caso, dado que solo tenía 2 líneas, parecía justo duplicarlo.

+0

@robrich ... gracias por su respuesta. Pero creo que hay algo de confusión aquí. NO QUIERO regresar hasta que haya iterado TODOS los objetos en stuObjList. Desde su código, parece que, tan pronto como encuentre el primer objeto que coincida con la condición IF, la función volverá. ¿Hay algo que hacer? –

+0

Entonces tiene razón, esta solución no funcionará tan bien, y tendremos que mirar el Deferred de jQuery. – robrich

1

dado el requisito, también se puede utilizar el método de subrayado "filtro" http://documentcloud.github.com/underscore/#filter

var honor_students = _.filter(stuObjList, function(stud) { return stu['honor_student'] != null }); 
if (honor_students.length === 0) { 
    process.nextTick(function() { callback(stuObjList); }); 
} else { 
    var honor_students_with_more_data = []; 
    for (var i = 0; i < honor_students.length; i++) { 
    db.collection("students").findOne({'_id' : honor_students[i]['_id'];}, function(err, origStuObj) { 
     // do something with retrieved data 
     honor_students_with_more_data.push(student_with_more_data); 
     if (honor_students_with_more_data.length === honor_students.length) { 
     process.nextTick(function() { callback(stuObjList); }); 
     } 
    } 
    } 
} 
0
And when the db call returns, the index 'j' is beyond the array index. 

Me parece que debe tomar una "copia" de j en cada iteración de bucle. Puedes hacer esto con cierres.

if(stuObjList[j]['honor_student'] != null) 
{ 

    (function(j_copy){ 
     db.collection("students").findOne({'_id' : stuObjList[j_copy]['_id'];}, function(err, origStuObj) 
     { 
      var marker = stuObjList[j_copy]['_id']; 
      var major = stuObjList[j_copy]['major']; 
     }); 
    })(j) 

} 

De esta forma está guardando el estado de j en cada iteración. Este estado se guarda dentro de cada IIFE. Tendrás tantos estados guardados como los bucles for. Cuando el DB devuelve:

var marker = stuObjList[j_copy]['_id']; 

j_copy mantendrá el valor de la j original, que lo ha hecho en el momento de

if(stuObjList[j]['honor_student'] != null) 

Sé que mis habilidades explican son muy malas, pero espero que puedan entiende lo que digo.

Edit: De esta manera estamos utilizando la función invocada inmediatamente y su alcance para mantener una copia privada separada de j. En cada iteración, se crea un nuevo IIFE con su propio ámbito privado. En este ámbito, en cada iteración hacemos j_copy = j.Y este j_copy se puede usar dentro de IIFE sin que el bucle for lo sobrescriba cada vez.

Cuestiones relacionadas