JavaScript, Node.js: ¿Array.forEach es asíncrono?


Respuestas:


392

No, está bloqueando. Echa un vistazo a la especificación del algoritmo .

Sin embargo, una implementación quizás más fácil de entender se da en MDN :

if (!Array.prototype.forEach)
{
  Array.prototype.forEach = function(fun /*, thisp */)
  {
    "use strict";

    if (this === void 0 || this === null)
      throw new TypeError();

    var t = Object(this);
    var len = t.length >>> 0;
    if (typeof fun !== "function")
      throw new TypeError();

    var thisp = arguments[1];
    for (var i = 0; i < len; i++)
    {
      if (i in t)
        fun.call(thisp, t[i], i, t);
    }
  };
}

Si tiene que ejecutar una gran cantidad de código para cada elemento, debe considerar usar un enfoque diferente:

function processArray(items, process) {
    var todo = items.concat();

    setTimeout(function() {
        process(todo.shift());
        if(todo.length > 0) {
            setTimeout(arguments.callee, 25);
        }
    }, 25);
}

y luego llamarlo con:

processArray([many many elements], function () {lots of work to do});

Esto sería sin bloqueo entonces. El ejemplo está tomado de JavaScript de alto rendimiento .

Otra opción podrían ser los trabajadores web .


37
Si está usando Node.js, también considere usar process.nextTick en lugar de setTimeout
Marcello Bastea-Forte

28
técnicamente, forEach no está "bloqueando", ya que la CPU nunca se duerme. Es síncrono y está vinculado a la CPU, lo que puede parecer un "bloqueo" cuando espera que la aplicación de nodo responda a los eventos.
Dave Dopson el

3
async sería probablemente una solución más apropiada aquí (de hecho, ¡acabo de ver que alguien publicó eso como respuesta!).
James

66
Confié en esta respuesta, pero parece estar mal en algunos casos. forEachno no bloquear en awaitdeclaraciones por ejemplo, y usted debe más bien utilizar un forbucle: stackoverflow.com/questions/37962880/...
Richard

3
@ Richard: por supuesto. Solo puedes usar funciones awaitinternas async. Pero forEachno sabe qué son las funciones asíncronas. Tenga en cuenta que las funciones asíncronas son solo funciones que devuelven una promesa. ¿Esperaría forEachmanejar una promesa devuelta de la devolución de llamada? forEachignora completamente el valor de retorno de la devolución de llamada. Solo podría manejar una devolución de llamada asíncrona si fuera asíncrona.
Felix Kling

80

Si necesita una versión asíncrona Array.forEachy similar, están disponibles en el módulo 'async' de Node.js: http://github.com/caolan/async ... como beneficio adicional, este módulo también funciona en el navegador .

async.each(openFiles, saveFile, function(err){
    // if any of the saves produced an error, err would equal that error
});

2
Si necesita asegurarse de que la operación asíncrona se ejecute solo para un elemento a la vez (en el orden de la colección) , debe usar eachSeriesen su lugar.
matpop

@ John Kennedy ¡Te he visto antes!
Xsmael

16

Hay un patrón común para hacer un cálculo realmente pesado en Node que puede ser aplicable a usted ...

El nodo es de un solo subproceso (como una opción de diseño deliberada, consulte ¿Qué es Node.js? ); Esto significa que solo puede utilizar un solo núcleo. Las cajas modernas tienen 8, 16 o incluso más núcleos, por lo que esto podría dejar el 90% de la máquina inactiva. El patrón común para un servicio REST es iniciar un proceso de nodo por núcleo y colocarlos detrás de un equilibrador de carga local como http://nginx.org/ .

Bifurcar a un niño : para lo que está tratando de hacer, hay otro patrón común, bifurcar un proceso de niño para hacer el trabajo pesado. Lo bueno es que el proceso secundario puede hacer cálculos pesados ​​en segundo plano mientras que el proceso principal responde a otros eventos. El problema es que no puede / no debe compartir memoria con este proceso secundario (no sin MUCHAS contorsiones y algún código nativo); Tienes que pasar mensajes. Esto funcionará maravillosamente si el tamaño de sus datos de entrada y salida es pequeño en comparación con el cálculo que debe realizarse. Incluso puede iniciar un proceso hijo de node.js y usar el mismo código que estaba usando anteriormente.

Por ejemplo:

var child_process = require ('child_process');
función run_in_child (array, cb) {
    var process = child_process.exec ('node libfn.js', function (err, stdout, stderr) {
        salida var = JSON.parse (stdout);
        cb (err, salida);
    });
    process.stdin.write (JSON.stringify (array), 'utf8');
    process.stdin.end ();
}

11
Para que quede claro ... El nodo no es de un solo subproceso, pero la ejecución de su JavaScript sí. IO y lo que no se ejecuta en hilos separados.
Brad

3
@Brad, tal vez. Eso depende de la implementación. Con el soporte adecuado del kernel, la interfaz entre Node y el kernel puede basarse en eventos: kqueue (mac), epoll (linux), puertos de finalización de E / S (windows). Como alternativa, un grupo de hilos también funciona. Sin embargo, su punto básico es correcto. La implementación de Nodo de bajo nivel puede tener múltiples hilos. Pero NUNCA los expondrán directamente a JS userland ya que eso rompería todo el modelo de lenguaje.
Dave Dopson el

44
Correcto, solo estoy aclarando porque el concepto ha confundido a muchos.
Brad

6

Array.forEachestá destinado a calcular cosas que no esperan, y no se gana nada haciendo que los cálculos sean asíncronos en un bucle de eventos (los trabajadores web agregan multiprocesamiento, si necesita cómputo multinúcleo). Si desea esperar a que finalicen varias tareas, use un contador, que puede ajustar en una clase de semáforo.


5

Editar 2018-10-11: Parece que hay una buena posibilidad de que el estándar descrito a continuación no se cumpla, considere la canalización como una alternativa (no se comporta exactamente igual pero los métodos podrían implementarse de manera similar).

Esta es exactamente la razón por la que estoy entusiasmado con es7, en el futuro podrá hacer algo como el código a continuación (algunas de las especificaciones no están completas, así que úselas con precaución, intentaré mantener esto actualizado). Pero, básicamente, utilizando el nuevo operador :: bind, podrá ejecutar un método en un objeto como si el prototipo del objeto contiene el método. por ejemplo, [Object] :: [Method] donde normalmente llamarías a [Object]. [ObjectsMethod]

Tenga en cuenta que para hacer esto hoy (24 de julio-16) y que funcione en todos los navegadores, necesitará transpilar su código para la siguiente funcionalidad: Importar / Exportar , Funciones de flecha , Promesas , Asíncrono / Esperar y lo más importante , enlace de funciones . El siguiente código podría modificarse para usar solo la función bind si es necesario, toda esta funcionalidad está perfectamente disponible hoy en día usando babel .

YourCode.js (donde ' mucho trabajo por hacer ' simplemente debe devolver una promesa, resolviéndola cuando se realiza el trabajo asincrónico).

import { asyncForEach } from './ArrayExtensions.js';

await [many many elements]::asyncForEach(() => lots of work to do);

ArrayExtensions.js

export function asyncForEach(callback)
{
    return Promise.resolve(this).then(async (ar) =>
    {
        for(let i=0;i<ar.length;i++)
        {
            await callback.call(ar, ar[i], i, ar);
        }
    });
};

export function asyncMap(callback)
{
    return Promise.resolve(this).then(async (ar) =>
    {
        const out = [];
        for(let i=0;i<ar.length;i++)
        {
            out[i] = await callback.call(ar, ar[i], i, ar);
        }
        return out;
    });
};

1

Esta es una función asíncrona corta para usar sin requerir libs de terceros

Array.prototype.each = function (iterator, callback) {
    var iterate = function () {
            pointer++;
            if (pointer >= this.length) {
                callback();
                return;
            }
            iterator.call(iterator, this[pointer], iterate, pointer);
    }.bind(this),
        pointer = -1;
    iterate(this);
};

¿Cómo es esto asíncrono? AFAIK #call se ejecutará inmediatamente?
Giles Williams

1
Por supuesto, de inmediato, pero tiene la función de devolución de llamada para saber cuándo se completarán todas las iteraciones. Aquí el argumento "iterador" es una función asíncrona de estilo de nodo con devolución de llamada. Es similar a async. Cada método
Rax Wunter

3
No veo cómo esto es asíncrono. llamar o aplicar son sincrónicos. Tener una devolución de llamada no lo hace asíncrono
adrianvlupu

en javascript cuando las personas dicen asíncrono, significan que la ejecución del código no bloquea el bucle principal del evento (es decir, no hace que el proceso se atasque en una línea de código). simplemente poner una devolución de llamada no hace que el código sea asíncrono, tiene que utilizar alguna forma de liberación de bucle de eventos, como setTimeout o setInterval. desde el momento en que los espera, se puede ejecutar otro código sin interrupciones.
vasilevich

0

Hay un paquete en npm para asincrónico fácil para cada bucle .

var forEachAsync = require('futures').forEachAsync;

// waits for one request to finish before beginning the next 
forEachAsync(['dogs', 'cats', 'octocats'], function (next, element, index, array) {
  getPics(element, next);
  // then after all of the elements have been handled 
  // the final callback fires to let you know it's all done 
  }).then(function () {
    console.log('All requests have finished');
});

También otra variación para AllAsync


0

Es posible codificar incluso la solución como esta, por ejemplo:

 var loop = function(i, data, callback) {
    if (i < data.length) {
        //TODO("SELECT * FROM stackoverflowUsers;", function(res) {
            //data[i].meta = res;
            console.log(i, data[i].title);
            return loop(i+1, data, errors, callback);
        //});
    } else {
       return callback(data);
    }
};

loop(0, [{"title": "hello"}, {"title": "world"}], function(data) {
    console.log("DONE\n"+data);
});

Por otro lado, es mucho más lento que un "para".

De lo contrario, la excelente biblioteca Async puede hacer esto: https://caolan.github.io/async/docs.html#each


0

Aquí hay un pequeño ejemplo que puede ejecutar para probarlo:

[1,2,3,4,5,6,7,8,9].forEach(function(n){
    var sum = 0;
    console.log('Start for:' + n);
    for (var i = 0; i < ( 10 - n) * 100000000; i++)
        sum++;

    console.log('Ended for:' + n, sum);
});

Producirá algo como esto (si toma mucho menos / mucho tiempo, aumenta / disminuye el número de iteraciones):

(index):48 Start for:1
(index):52 Ended for:1 900000000
(index):48 Start for:2
(index):52 Ended for:2 800000000
(index):48 Start for:3
(index):52 Ended for:3 700000000
(index):48 Start for:4
(index):52 Ended for:4 600000000
(index):48 Start for:5
(index):52 Ended for:5 500000000
(index):48 Start for:6
(index):52 Ended for:6 400000000
(index):48 Start for:7
(index):52 Ended for:7 300000000
(index):48 Start for:8
(index):52 Ended for:8 200000000
(index):48 Start for:9
(index):52 Ended for:9 100000000
(index):45 [Violation] 'load' handler took 7285ms

Esto sucederá incluso si va a escribir async.foreach o cualquier otro método paralelo. Debido a que for loop no es un proceso IO, Nodejs siempre lo hará de forma sincrónica.
Sudhanshu Gaur

-2

Use Promise.each of bluebird library.

Promise.each(
Iterable<any>|Promise<Iterable<any>> input,
function(any item, int index, int length) iterator
) -> Promise

Este método itera sobre una matriz, o una promesa de una matriz, que contiene promesas (o una combinación de promesas y valores) con la función iteradora dada con la firma (valor, índice, longitud) donde el valor es el valor resuelto de un promesa respectiva en la matriz de entrada. La iteración ocurre en serie.Si la función de iterador devuelve una promesa o un thenable, entonces se espera el resultado de la promesa antes de continuar con la próxima iteración. Si se rechaza cualquier promesa en la matriz de entrada, también se rechaza la promesa devuelta.

Si todas las iteraciones se resuelven correctamente, Promise.each se resuelve en la matriz original sin modificaciones . Sin embargo, si una iteración rechaza o contiene errores, Promise.each deja de ejecutarse inmediatamente y no procesa más iteraciones. El error o el valor rechazado se devuelve en este caso en lugar de la matriz original.

Este método está destinado a ser utilizado para efectos secundarios.

var fileNames = ["1.txt", "2.txt", "3.txt"];

Promise.each(fileNames, function(fileName) {
    return fs.readFileAsync(fileName).then(function(val){
        // do stuff with 'val' here.  
    });
}).then(function() {
console.log("done");
});
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.