JavaScript promesa ES6 para bucle

for (let i = 0; i < 10; i++) {
    const promise = new Promise((resolve, reject) => {
        const timeout = Math.random() * 1000;
        setTimeout(() => {
            console.log(i);
        }, timeout);
    });

    // TODO: Chain this promise to the previous one (maybe without having it running?)
}

Lo anterior dará la siguiente salida aleatoria:

6
9
4
8
5
1
7
2
3
0

La tarea es simple: Asegúrese de que cada promesa funcione sólo después del otro (.then()).

Por alguna razón, no pude encontrar una manera de hacerlo.

He probado las funciones del generador (yield), probó funciones simples que devuelven una promesa, pero al final del día siempre se reduce al mismo problema: El bucle es sincrónico.

Con async Simplemente usaría async.series().

¿Cómo lo resuelves?

Pregunta hecha hace 7 años, 3 meses, 26 días - Por phpphoenix


11 Respuestas:

  • Como ya insinuaste en tu pregunta, tu código crea todas las promesas sincronizadamente. En cambio, sólo deben crearse en el momento en que el anterior se resuelva.

    Segundo, cada promesa que se crea con new Promise necesita ser resuelto con una llamada resolve (o reject). Esto debe hacerse cuando el temporizador expira. Eso desencadenará cualquier then Callback que tendrías en esa promesa. Y tal then callback (o await) es una necesidad para implementar la cadena.

    Con esos ingredientes, hay varias maneras de realizar este encadenamiento asincrónico:

    1. Con un for bucle que comienza con una promesa de resolución inmediata

    2. Con Array#reduce que comienza con una promesa de resolución inmediata

    3. Con una función que se pasa a sí misma como callback de resolución

    4. Con ECMAScript2017 async / await sintaxis

    5. Con ECMAScript2020 for await...of sintaxis

    Pero permítanme presentar primero una función muy útil y genérica.

    Promisfying setTimeout

    Uso setTimeout está bien, pero en realidad necesitamos una promesa que resuelve cuando el temporizador expira. Así que vamos a crear tal función: esto se llama promisificación una función, en este caso promisificaremos setTimeout. Mejorará la legibilidad del código, y se puede utilizar para todas las opciones anteriores:

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    

    Vea un fragmento y comentarios para cada una de las opciones a continuación.

    1. Con for

    Tú. puede use a for bucle, pero debes asegurarte de que no crea todas las promesas sincronizadamente. En su lugar creas una promesa inicial de resolución inmediata, y luego encadena nuevas promesas como las anteriores resuelven:

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    for (let i = 0, p = Promise.resolve(); i < 10; i++) {
        p = p.then(() => delay(Math.random() * 1000))
             .then(() => console.log(i));
    }

    Así que este código crea una cadena larga then llamadas. La variable p sólo sirve para no perder el rastro de esa cadena, y permitir una próxima iteración del bucle para continuar en la misma cadena. Los callbacks comenzarán a ejecutar después de que el bucle sincronizado haya completado.

    Es importante que el then- Retrocede. retornos la promesa de que delay() crea: esto asegurará la cadena asincrónica.

    2. Con reduce

    Este es sólo un enfoque más funcional de la estrategia anterior. Cree un array con la misma longitud que la cadena que desea ejecutar, y comience con una promesa de resolución inmediata:

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    [...Array(10)].reduce( (p, _, i) => 
        p.then(() => delay(Math.random() * 1000))
         .then(() => console.log(i))
    , Promise.resolve() );

    Esto es probablemente más útil cuando realmente han tenido un array con datos a utilizar en las promesas.

    3. Con una función que se pasa como resolución-callback

    Aquí creamos una función y la llamamos inmediatamente. Crea la primera promesa sincronizada. Cuando se resuelve, la función se llama de nuevo:

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    (function loop(i) {
        if (i >= 10) return; // all done
        delay(Math.random() * 1000).then(() => {
            console.log(i);
            loop(i+1);
        });
    })(0);

    Esto crea una función llamada loop, y al final del código se puede ver que se llama inmediatamente con el argumento Este es el contador, y el i argumento. La función creará una nueva promesa si ese contador sigue por debajo de 10, de lo contrario la cadena se detiene.

    Cuando delay() resuelve, activará el then callback que llamará a la función de nuevo.

    4. Con async/await

    Motores modernos de JS soporte esta sintaxis:

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    (async function loop() {
        for (let i = 0; i < 10; i++) {
            await delay(Math.random() * 1000);
            console.log(i);
        }
    })();

    Puede parecer extraño. Parece que... como las promesas se crean sincronizadamente, pero en realidad las async función retornos cuando ejecuta la primera await. Cada vez que una promesa esperada resuelve, el contexto de funcionamiento de la función es restaurado, y procede después de la await, hasta que se encuentre con el siguiente, y así continúa hasta que el bucle termine.

    5. Con for await...of

    Con EcmaScript 2020, el for await...of encontró su camino a los motores JavaScript modernos. Aunque realmente no reduce el código en este caso, permite aislar la definición de la cadena de intervalos aleatorios de la iteración real de él:

    const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    async function * randomDelays(count, max) {
        for (let i = 0; i < count; i++) yield delay(Math.random() * max).then(() => i);
    }
    
    (async function loop() {
        for await (let i of randomDelays(10, 1000)) console.log(i);
    })();

    Respondida el Oct 30, 2016 a las 12:22 - por geekglitcher

    Votos positivos: 0 | Votos negativos: 0

  • Puedes usar async/await para esto. Yo explicaría más, pero no hay nada realmente. Es sólo un regular for pero añadí el bucle await palabra clave antes de la construcción de su promesa

    Lo que me gusta de esto es que su promesa puede resolver un valor normal en lugar de tener un efecto secundario como su código (o otras respuestas aquí) incluyen. Esto te da poderes como La leyenda de Zelda: Un enlace al pasado donde puedes afectar las cosas en el Mundo de la Luz y el Mundo Oscuro – es decir, puedes trabajar fácilmente con datos antes/después de que los datos prometidos estén disponibles sin tener que recurrir a funciones muy anidadas, otras estructuras de control no inteligentes o estúpidos IIFEs.

    // where DarkWorld is in the scary, unknown future
    // where LightWorld is the world we saved from Ganondorf
    LightWorld ... await DarkWorld
    

    Así que esto es lo que parecerá...

    async function someProcedure (n) {
      for (let i = 0; i < n; i++) {
        const t = Math.random() * 1000
        const x = await new Promise(r => setTimeout(r, t, i))
        console.log (i, x)
      }
      return 'done'
    }
    
    someProcedure(10)
      .then(console.log)
      .catch(console.error)

    0 0
    1 1
    2 2
    3 3
    4 4
    5 5
    6 6
    7 7
    8 8
    9 9
    done
    

    Mira cómo no tenemos que lidiar con ese molesto .then ¿llamar dentro de nuestro procedimiento? Y async palabra clave asegurará automáticamente que Promise es devuelto, así que podemos encadenar un .then llame al valor devuelto. Esto nos establece para un gran éxito: ejecutar la secuencia de n Promesas, entonces hacer algo importante – como mostrar un mensaje de éxito/error.

    Respondida el Oct 30, 2016 a las 12:30 - por syntaxsenseie7e4

    Votos positivos: 0 | Votos negativos: 0

  • Basado en la excelente respuesta de trincot, escribí una función reutilizable que acepta un manejador para ejecutar sobre cada artículo en un array. La función misma devuelve una promesa que le permite esperar hasta que el bucle haya terminado y la función de manejador que usted pasa también puede devolver una promesa.

    loop(items, handler) : Promesa

    Me tomó algún tiempo corregirlo, pero creo que el siguiente código será usable en muchas situaciones de promesas-aplausos.

    Copiar código listo:

    // SEE https://stackoverflow.com/a/46295049/286685
    const loop = (arr, fn, busy, err, i=0) => {
      const body = (ok,er) => {
        try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
        catch(e) {er(e)}
      }
      const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
      const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
      return busy ? run(busy,err) : new Promise(run)
    }
    

    Usage

    Para utilizarlo, llámalo con el array para bucle sobre como el primer argumento y la función del manejador como el segundo. No pase parámetros para los argumentos tercero, cuarto y quinto, se utilizan internamente.

    const loop = (arr, fn, busy, err, i=0) => {
      const body = (ok,er) => {
        try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
        catch(e) {er(e)}
      }
      const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
      const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
      return busy ? run(busy,err) : new Promise(run)
    }
    
    const items = ['one', 'two', 'three']
    
    loop(items, item => {
      console.info(item)
    })
    .then(() => console.info('Done!'))

    Casos de uso avanzado

    Veamos la función del manejador, bucles anidados y manejo de errores.

    handler(current, index, all)

    El manejador pasa 3 argumentos. El ítem actual, el índice del ítem actual y la matriz completa que se está superando. Si la función del manejador necesita hacer trabajo asinc, puede devolver una promesa y la función del bucle esperará a que la promesa de resolver antes de comenzar la próxima iteración. Puedes anidar invocaciones de bucle y todas las obras como se espera.

    const loop = (arr, fn, busy, err, i=0) => {
      const body = (ok,er) => {
        try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
        catch(e) {er(e)}
      }
      const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
      const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
      return busy ? run(busy,err) : new Promise(run)
    }
    
    const tests = [
      [],
      ['one', 'two'],
      ['A', 'B', 'C']
    ]
    
    loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
      console.info('Performing test ' + idx)
      return loop(test, (testCase) => {
        console.info(testCase)
      })
      .then(testNext)
      .catch(testFailed)
    }))
    .then(() => console.info('All tests done'))

    Manejo de errores

    Muchos ejemplos prometedores Miré la ruptura cuando se produce una excepción. Obtener esta función para hacer lo correcto fue bastante complicado, pero en lo que puedo decir que está funcionando ahora. Asegúrese de añadir un manejador de captura a cualquier bucle interior e invocar la función de rechazo cuando sucede. Por ejemplo:

    const loop = (arr, fn, busy, err, i=0) => {
      const body = (ok,er) => {
        try {const r = fn(arr[i], i, arr); r && r.then ? r.then(ok).catch(er) : ok(r)}
        catch(e) {er(e)}
      }
      const next = (ok,er) => () => loop(arr, fn, ok, er, ++i)
      const run  = (ok,er) => i < arr.length ? new Promise(body).then(next(ok,er)).catch(er) : ok()
      return busy ? run(busy,err) : new Promise(run)
    }
    
    const tests = [
      [],
      ['one', 'two'],
      ['A', 'B', 'C']
    ]
    
    loop(tests, (test, idx, all) => new Promise((testNext, testFailed) => {
      console.info('Performing test ' + idx)
      loop(test, (testCase) => {
        if (idx == 2) throw new Error()
        console.info(testCase)
      })
      .then(testNext)
      .catch(testFailed)  //  <--- DON'T FORGET!!
    }))
    .then(() => console.error('Oops, test should have failed'))
    .catch(e => console.info('Succesfully caught error: ', e))
    .then(() => console.info('All tests done'))

    UPDATE: Paquete NPM

    Desde que escribí esta respuesta, cambié el código anterior en un paquete NPM.

    for-async

    Instala

    npm install --save for-async
    

    Importación

    var forAsync = require('for-async');  // Common JS, or
    import forAsync from 'for-async';
    

    Usage (async)

    var arr = ['some', 'cool', 'array'];
    forAsync(arr, function(item, idx){
      return new Promise(function(resolve){
        setTimeout(function(){
          console.info(item, idx);
          // Logs 3 lines: `some 0`, `cool 1`, `array 2`
          resolve(); // <-- signals that this iteration is complete
        }, 25); // delay 25 ms to make async
      })
    })
    

    Vea el paquete readme para más detalles.

    Respondida el Oct 30, 2016 a las 12:40 - por techtrailblazer8d2c

    Votos positivos: 0 | Votos negativos: 0

  • Si usted está limitado a ES6, la mejor opción es Promesa todo. Promise.all(array) también devuelve una serie de promesas después de ejecutar con éxito todas las promesas en array argumento. Supongamos que, si desea actualizar muchos registros de estudiantes en la base de datos, el siguiente código muestra el concepto de la promesa. todo en ese caso...

    let promises = students.map((student, index) => {
        //where students is a db object
        student.rollNo = index + 1;
        student.city = 'City Name';
        //Update whatever information on student you want
        return student.save();
    });
    Promise.all(promises).then(() => {
        //All the save queries will be executed when .then is executed
        //You can do further operations here after as all update operations are completed now
    });
    

    El mapa es sólo un método de ejemplo para el bucle. También puede utilizar for o forin o forEach bucle. Así que el concepto es bastante simple, empezar el bucle en el que desea hacer operaciones de asinc a granel. Empuje cada declaración de operación asinc en un array declarado fuera del alcance de ese bucle. Después de que el bucle termine, ejecute la declaración Promesa con el conjunto preparado de tales consultas/promisos como argumento.

    El concepto básico es que el bucle de javascript es sincronizado, mientras que la llamada de la base de datos es asinc y utilizamos el método de empuje en bucle que también es sincronizado. Así que el problema del comportamiento asincrónico no ocurre dentro del bucle.

    Respondida el Oct 30, 2016 a las 12:46 - por pixelprodigy

    Votos positivos: 0 | Votos negativos: 0

  • Aquí valen mis 2 centavos:

    • función resufrible forpromise()
    • emula un clásico para bucle
    • permite una salida temprana basada en la lógica interna, devolviendo un valor
    • puede recoger una serie de resultados pasados en resolución/next/collect
    • defaults to start=0,increment=1
    • excepciones tiradas dentro del bucle se capturan y pasan a .catch()

        function forpromise(lo, hi, st, res, fn) {
            if (typeof res === 'function') {
                fn = res;
                res = undefined;
            }
            if (typeof hi === 'function') {
                fn = hi;
                hi = lo;
                lo = 0;
                st = 1;
            }
            if (typeof st === 'function') {
                fn = st;
                st = 1;
            }
            return new Promise(function(resolve, reject) {
    
                (function loop(i) {
                    if (i >= hi) return resolve(res);
                    const promise = new Promise(function(nxt, brk) {
                        try {
                            fn(i, nxt, brk);
                        } catch (ouch) {
                            return reject(ouch);
                        }
                    });
                    promise.
                    catch (function(brkres) {
                        hi = lo - st;
                        resolve(brkres)
                    }).then(function(el) {
                        if (res) res.push(el);
                        loop(i + st)
                    });
                })(lo);
    
            });
        }
    
    
        //no result returned, just loop from 0 thru 9
        forpromise(0, 10, function(i, next) {
            console.log("iterating:", i);
            next();
        }).then(function() {
    
    
            console.log("test result 1", arguments);
    
            //shortform:no result returned, just loop from 0 thru 4
            forpromise(5, function(i, next) {
                console.log("counting:", i);
                next();
            }).then(function() {
    
                console.log("test result 2", arguments);
    
    
    
                //collect result array, even numbers only
                forpromise(0, 10, 2, [], function(i, collect) {
                    console.log("adding item:", i);
                    collect("result-" + i);
                }).then(function() {
    
                    console.log("test result 3", arguments);
    
                    //collect results, even numbers, break loop early with different result
                    forpromise(0, 10, 2, [], function(i, collect, break_) {
                        console.log("adding item:", i);
                        if (i === 8) return break_("ending early");
                        collect("result-" + i);
                    }).then(function() {
    
                        console.log("test result 4", arguments);
    
                        // collect results, but break loop on exception thrown, which we catch
                        forpromise(0, 10, 2, [], function(i, collect, break_) {
                            console.log("adding item:", i);
                            if (i === 4) throw new Error("failure inside loop");
                            collect("result-" + i);
                        }).then(function() {
    
                            console.log("test result 5", arguments);
    
                        }).
                        catch (function(err) {
    
                            console.log("caught in test 5:[Error ", err.message, "]");
    
                        });
    
                    });
    
                });
    
    
            });
    
    
    
        });

    Respondida el Oct 30, 2016 a las 12:55 - por byteexplorer

    Votos positivos: 0 | Votos negativos: 0

  • In ES6, debe usar 'para esperar':

    (async () => {
      for await (const num of asyncIterable) {
        console.log(num);
      }
      // My action here
    })();
    

    Para más información, vea esto por espera... de.

    Respondida el Oct 30, 2016 a las 13:03 - por scriptsorcererf493

    Votos positivos: 0 | Votos negativos: 0

  • Veo las respuestas anteriores y me siento confundido. Y codificaba lo siguiente por la inspiración de las respuestas. Creo que su lógica es más obvia, llamo a la función para reemplazar original para bucle:

    async function pointToCountry(world, data) { // Data is for loop array
      if (data.length > 0) { // For condition
        const da = data.shift(); // Get current data and modified data one row code
        // Some business logic
        msg = da.info
        pointofView(world, da);
        // Await the current task
        await new Promise(r => setTimeout(_ => {
          r() // Resolve and finish the current task
        }, 5000))
        // Call itself and enter the next loop
        pointToCountry(world, data)
      } else { // Business logic after all tasks
        pointofView(world, { longitude: 0, latitude: 0 });
        world.controls().autoRotate = true;
      }
    }
    

    Respondida el Oct 30, 2016 a las 13:13 - por bughunterx

    Votos positivos: 0 | Votos negativos: 0

  •     // This is my main function - calculate all project by city
           const projectCity = async (req, res, next) => {
                try {         
                    let record = [];            
                    let cityList = await Cityodel.find({active:true});
                
                        for (let j = 0; j < cityList.length; j++) {
            
                        let arr = [];
                    
                        let projectList   = await getProduct(cityList[j]._id)
            
                        arr.push({
                            _id:cityList[j]._id,
                            name:cityList[j].name,
                            projectList:projectList
                        })
              
                        record.push(arr);                
                    }
            
                    return res.status(200).send({ 
                        status: CONSTANT.REQUESTED_CODES.SUCCESS,
                        result: record });
                } catch (error) {
                    return res.status(400).json(UTILS.errorHandler(error));
                }
            };
                
            
            async function getProduct(city){       
                let projectList = await ProjectModel.find({city:city});      
                return projectList;        
            }
    

    Respondida el Oct 30, 2016 a las 13:22 - por pixelprodigy

    Votos positivos: 0 | Votos negativos: 0

  • Aquí está mi intento de trabajo utilizando la recursión:

    function promisesWithRecursion() {
        const urlArray = [myUrl1, myUrl2, myUrl3]; //... and many more
        let i = 0;
        let call = ( () => fetch(urlArray[i], { method: "GET"})
        .then(response => response.text())
        .then(text => {
            //onSuccess()
            //console.log("response: " + text);
            document.getElementById("output").innerHTML += text;
            if(text.includes("ERROR")) {
                console.log("Error in url index " + i);
                console.log(urlArray[i]);
                return null;
            } else {
                nextUrl = urlArray[++i] ? urlArray[i] : null;
                if(!nextUrl) {
                    return null;
                } else {
                    return call();
                }
            }
        }
        )
        .catch(console.error.bind(console))
        );
        call();
    }
    

    De esta manera las promesas se ejecutan una tras otra y las llamadas se bloquean si se lanzan errores; también lo intenté con bucles como

    let prom = Promise.resolve();
    for (const url of urlArray) {
        prom.then(() => fetch(url, { method: "GET"})
        .then(response => response.text())
        .then(text => {
            document.getElementById("output").innerHTML += text;
            if(text.includes("ERROR")) {
                break;
            }
        }).catch(...) );
    }
    

    o

    for (let i = 0; i < urlArray.length; i++) ...
    

    pero no puedo hacerlo funcionar, todos están despedidos en paralelo.

    Respondida el Oct 30, 2016 a las 13:28 - por quantumquasar

    Votos positivos: 0 | Votos negativos: 0

  • En JavaScript, con ES6 y Promesas, una forma de hacer que cada promesa se ejecute en secuencia después de la anterior es utilizando recursión. Aquí te muestro cómo puedes modificar tu código para lograr ese comportamiento:

    javascript
    let i = 0;
    
    function createPromise() {
        return new Promise((resolve, reject) => {
            const timeout = Math.random() * 1000;
            setTimeout(() => {
                console.log(i);
                resolve();
            }, timeout);
        });
    }
    
    function runSequentialPromises() {
        if (i < 10) {
            i++;
            createPromise().then(() => {
                runSequentialPromises();
            });
        }
    }
    
    runSequentialPromises();
    

    En este código, creamos una función createPromise que devuelve una promesa con un temporizador como en tu ejemplo original. Luego tenemos una función runSequentialPromises que verifica si el índice i es menor que 10, y si es así, incrementa i, crea una promesa y luego llama a runSequentialPromises de forma recursiva después de que la promesa se resuelva.

    De esta manera, cada promesa se ejec

    Respondida el Oct 31, 2016 a las 12:20 - por Gpt

    Votos positivos: 0 | Votos negativos: 0

  • Para encadenar las promesas en un bucle y asegurarte de que se ejecuten secuencialmente, puedes utilizar async/await. Esto te permite escribir código asíncrono de forma síncrona.

    Aquí tienes el código actualizado con async/await:

    javascript
    async function main() {
      for (let i = 0; i < 10; i++) {
        const promise = new Promise((resolve, reject) => {
          const timeout = Math.random() * 1000;
          setTimeout(() => {
            console.log(i);
            resolve();
          }, timeout);
        });
    
        await promise;
      }
    }
    
    main();
    

    Cuando se utiliza async/await, el bucle for se ejecutará secuencialmente, esperando a que cada promesa se resuelva antes de pasar a la siguiente iteración. Esto garantizará que las promesas se ejecuten en el orden correcto y que la salida sea:

    
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    

    Respondida el Oct 31, 2016 a las 12:32 - por Gemini

    Votos positivos: 0 | Votos negativos: 0