javascript - sincronas - promesas typescript
Promesa de JavaScript ES6 para el bucle (5)
Aquí está mi valor de 2 centavos:
-
función
forpromise()
- emula un clásico para bucle
- permite una salida anticipada basada en la lógica interna, devolviendo un valor
- puede recopilar una serie de resultados pasados a resolve / next / collect
- el valor predeterminado es inicio = 0, incremento = 1
- las excepciones lanzadas dentro del bucle se capturan y se pasan a .catch ()
let promises = [];
students.map((student, index) => {
student.rollNo = index + 1;
student.city = ''City Name'';
//Update whatever information on student you want
promises.push(student.save());
//where save() is a function used to save data in mongoDB
});
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
});
Esta pregunta ya tiene una respuesta aquí:
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á el siguiente resultado aleatorio:
6
9
4
8
5
1
7
2
3
0
La tarea es simple: asegúrese de que cada promesa se ejecute solo después de la otra (
.then()
).
Por alguna razón, no pude encontrar una manera de hacerlo.
Probé las funciones del generador (
yield
), probé funciones simples que devuelven una promesa, pero al final del día siempre se trata del mismo problema:
el bucle es sincrónico
.
Con
async
simplemente usaría
async.series()
.
¿Cómo lo resuelves?
Basado en la excelente respuesta de trincot, escribí una función reutilizable que acepta un controlador para ejecutar cada elemento en una matriz. La función en sí misma devuelve una promesa que le permite esperar hasta que el ciclo haya terminado y la función de controlador que pasa también puede devolver una promesa.
bucle (elementos, controlador): promesa
Me tomó algo de tiempo hacerlo bien, pero creo que el siguiente código se podrá usar en muchas situaciones de promesa.
Código listo para copiar y pegar:
// SEE https://.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)
}
Uso
Para usarlo, llámelo con la matriz para recorrer como primer argumento y la función de controlador como segundo. No pase parámetros para los argumentos tercero, cuarto y quinto, se usan 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 controlador, los bucles anidados y el manejo de errores.
manejador (actual, índice, todo)
El controlador obtiene 3 argumentos. El elemento actual, el índice del elemento actual y la matriz completa que se está pasando. Si la función del controlador necesita hacer un trabajo asíncrono, puede devolver una promesa y la función de bucle esperará a que la promesa se resuelva antes de comenzar la siguiente iteración. Puede anidar invocaciones de bucle y todo funciona 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 de bucles de promesa que miré se descomponen cuando ocurre una excepción. Conseguir que esta función hiciera lo correcto fue bastante complicado, pero por lo que puedo decir, está funcionando ahora. Asegúrese de agregar un controlador catch a cualquier bucle interno e invoque la función de rechazo cuando ocurra. P.ej:
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''))
ACTUALIZACIÓN: paquete NPM
Desde que escribí esta respuesta, convertí el código anterior en un paquete NPM.
for-async
Instalar en pc
npm install --save for-async
Importar
var forAsync = require(''for-async''); // Common JS, or
import forAsync from ''for-async'';
Uso (asíncrono)
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 archivo Léame para más detalles.
Como ya insinuó en su pregunta, su código crea todas las promesas sincrónicamente. En cambio, solo deben crearse en el momento en que se resuelva el anterior.
En segundo lugar, cada promesa que se crea con una
new Promise
debe resolverse con un llamado a
resolve
(o
reject
).
Esto debe hacerse cuando expire el temporizador.
Eso activará cualquier devolución de llamada que tenga en esa promesa.
Y tal devolución de llamada (o
await
) es una necesidad para implementar la cadena.
Con esos ingredientes, hay varias formas de realizar este encadenamiento asincrónico:
-
Con un bucle
for
que comienza con una promesa de resolución inmediata -
Con
Array#reduce
eso comienza con una promesa de resolución inmediata -
Con una función que se transmite como devolución de llamada de resolución
-
Con la sintaxis
async
/ enawait
ECMAScript2017 -
Con el ECMAScript2020 propuesto
for await...of
sintaxis
Vea un fragmento y comentarios para cada una de estas opciones a continuación.
1. Con
for
Puede usar un bucle
for
, pero debe asegurarse de que no se ejecute
new Promise
sincrónicamente.
En su lugar, crea una promesa inicial de resolución inmediata y luego encadena nuevas promesas a medida que se resuelven las anteriores:
for (let i = 0, p = Promise.resolve(); i < 10; i++) {
p = p.then(_ => new Promise(resolve =>
setTimeout(function () {
console.log(i);
resolve();
}, Math.random() * 1000)
));
}
2. Con
reduce
Este es solo un enfoque más funcional de la estrategia anterior. Crea una matriz con la misma longitud que la cadena que desea ejecutar y comienza con una promesa de resolución inmediata:
[...Array(10)].reduce( (p, _, i) =>
p.then(_ => new Promise(resolve =>
setTimeout(function () {
console.log(i);
resolve();
}, Math.random() * 1000)
))
, Promise.resolve() );
Esto probablemente sea más útil cuando en realidad tiene una matriz con datos para usar en las promesas.
3. Con una función que se pasa a sí misma como devolución de llamada de resolución
Aquí creamos una función y la llamamos de inmediato. Crea la primera promesa sincrónicamente. Cuando se resuelve, la función se llama nuevamente:
(function loop(i) {
if (i < 10) new Promise((resolve, reject) => {
setTimeout( () => {
console.log(i);
resolve();
}, Math.random() * 1000);
}).then(loop.bind(null, 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 0. Este es el contador y el argumento
i
.
La función creará una nueva promesa si ese contador aún está por debajo de 10, de lo contrario, el encadenamiento se detiene.
La llamada a
resolve()
activará la devolución de llamada, que volverá a llamar a la función.
loop.bind(null, i+1)
es solo una forma diferente de decir
_ => loop(i+1)
.
4. Con
async
/
await
Los motores JS modernos admiten esta sintaxis :
(async function loop() {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
console.log(i);
}
})();
Puede parecer extraño, ya que
parece
que las
new Promise()
llamadas
new Promise()
se ejecutan sincrónicamente, pero en realidad la función
async
regresa
cuando ejecuta la primera
await
.
Cada vez que se resuelve una promesa esperada, el contexto de ejecución de la función se restaura y continúa después de la
await
, hasta que encuentra la siguiente, y así continúa hasta que finaliza el ciclo.
Como puede ser una cosa común devolver una promesa basada en un tiempo de espera, podría crear una función separada para generar dicha promesa.
Esto se llama
promisificar
una función, en este caso
setTimeout
.
Puede mejorar la legibilidad del código:
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);
}
})();
5. Con
for await...of
Incluso más recientemente, la sintaxis
for await...of
llegó a algunos motores JavaScript.
Aunque realmente no reduce el código en este caso, permite aislar la definición de la cadena de intervalo aleatorio de la iteración real de la misma:
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);
})();
Puede usar
async/await
para esto.
Explicaría más, pero no hay nada realmente.
Es solo un ciclo
for
regular, pero agregué la palabra clave
await
antes de la construcción de su Promise
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 (u otras respuestas aquí) incluyen. Esto le da poderes como en The Legend of Zelda: A Link to the Past, donde puede afectar las cosas tanto en el mundo de la luz como en el mundo oscuro, es decir, puede trabajar fácilmente con los datos antes / después de que los datos prometidos estén disponibles sin tener que recurrir a funciones profundamente anidadas, otras estructuras de control difíciles de manejar o estúpidos IIFE .
// where DarkWorld is in the scary, unknown future
// where LightWorld is the world we saved from Ganondorf
LightWorld ... await DarkWorld
Así que así es como se verá ...
const someProcedure = async 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(x => console.log(x)) // => Promise
// 0 0
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5
// 6 6
// 7 7
// 8 8
// 9 9
// done
¿Ves cómo no tenemos que lidiar con ese molesto.
.then
llama dentro de nuestro procedimiento?
Y la palabra clave
async
garantizará automáticamente que se devuelva una
Promise
, de modo que podamos encadenar una llamada
.then
en el valor devuelto.
Esto nos prepara para un gran éxito: ejecute la secuencia de
n
Promesas,
luego
haga algo importante, como mostrar un mensaje de éxito / error.
Si está limitado a ES6, la mejor opción es Promesa de todo.
Promise.all(array)
también devuelve una serie de promesas después de ejecutar con éxito todas las promesas en el argumento de
array
.
Supongamos que si desea actualizar muchos registros de estudiantes en la base de datos, el siguiente código demuestra el concepto de Promise.
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, "]");
});
});
});
});
});
Map es solo un método de ejemplo para loop.
También puede usar
for
or
forin
o
forEach
loop.
Entonces, el concepto es bastante simple, inicie el ciclo en el que desea realizar operaciones asincrónicas masivas.
Empuje cada declaración de operación asíncrona en una matriz declarada fuera del alcance de ese bucle.
Una vez que se completa el ciclo, ejecute la declaración Promesa de todos con la matriz preparada de consultas / promesas como argumento.
El concepto básico es que el bucle de JavaScript es síncrono, mientras que la llamada a la base de datos es asíncrona y utilizamos el método push en el bucle que también está sincronizado. Entonces, el problema del comportamiento asincrónico no ocurre dentro del ciclo.