JavaScript, Node.js: является ли Array.forEach асинхронным?

378

У меня есть вопрос относительно нативной Array.forEachреализации JavaScript: он ведет себя асинхронно? Например, если я позвоню:

[many many elements].forEach(function () {lots of work to do})

Это будет неблокирующим?

Р. Гр.
источник

Ответы:

392

Нет, это блокировка. Посмотрите на спецификацию алгоритма .

Однако, возможно, более понятная реализация дана в 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);
    }
  };
}

Если вам нужно выполнить много кода для каждого элемента, вы должны рассмотреть возможность использования другого подхода:

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

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

а затем позвоните с помощью:

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

Это было бы неблокирующим тогда. Пример взят из высокопроизводительного JavaScript .

Другим вариантом могут быть веб-работники .

Феликс Клинг
источник
37
Если вы используете Node.js, также рассмотрите возможность использования process.nextTick вместо setTimeout
Marcello Bastea-Forte
28
технически forEach не «блокирует», так как процессор никогда не засыпает. Это синхронно и привязано к процессору, что может показаться «блокировкой», когда вы ожидаете, что приложение узла будет реагировать на события.
Дейв Допсон
3
Async будет, вероятно, более подходящим решением здесь (на самом деле только что видел кто-то опубликовал это в качестве ответа!).
Джеймс
6
Я доверял этому ответу, но в некоторых случаях он кажется неправильным. forEachэто не блок на awaitзаявления например , и вы скорее должны использовать forцикл: stackoverflow.com/questions/37962880/...
Ричард
3
@Richard: конечно. Вы можете использовать только awaitвнутри asyncфункции. Но forEachне знает, что такое асинхронные функции. Имейте в виду, что асинхронные функции - это просто функции, возвращающие обещание. Ожидаете ли вы forEachсправиться с обещанием, возвращенным после обратного вызова? forEachполностью игнорирует возвращаемое значение из обратного вызова. Он сможет обработать асинхронный обратный вызов только в том случае, если это будет сам асинхронный вызов.
Феликс Клинг
80

Если вам нужна асинхронная версия Array.forEachи аналогичные, они доступны в модуле «async» Node.js: http://github.com/caolan/async ... в качестве бонуса этот модуль также работает в браузере ,

async.each(openFiles, saveFile, function(err){
    // if any of the saves produced an error, err would equal that error
});
Caolan
источник
2
Если вам нужно убедиться, что асинхронная операция выполняется только для одного элемента за раз (в порядке сбора) , вы должны использовать eachSeriesвместо этого.
matpop
@JohnKennedy Я видел тебя раньше!
Xsmael
16

Существует общая схема выполнения действительно тяжелых вычислений в Node, которая может быть применима к вам ...

Node является однопоточным (в качестве преднамеренного выбора дизайна см. Что такое Node.js? ); это означает, что он может использовать только одно ядро. Современные боксы имеют 8, 16 или даже больше ядер, так что это может оставить 90 +% простоя машины. Обычный шаблон для службы REST - запуск одного процесса узла на ядро ​​и размещение его за локальным балансировщиком нагрузки, таким как http://nginx.org/ .

Форкинг ребенка - для того, что вы пытаетесь сделать, есть еще одна распространенная модель, которая заключается в том, что вы выполняете тяжелую работу над дочерним процессом. Положительным моментом является то, что дочерний процесс может выполнять тяжелые вычисления в фоновом режиме, в то время как ваш родительский процесс реагирует на другие события. Загвоздка в том, что вы не можете / не должны делить память с этим дочерним процессом (не без МНОГО потрясений и некоторого нативного кода); Вы должны передавать сообщения. Это будет прекрасно работать, если размер ваших входных и выходных данных будет небольшим по сравнению с вычислением, которое необходимо выполнить. Вы даже можете запустить дочерний процесс node.js и использовать тот же код, который вы использовали ранее.

Например:

var child_process = require ('child_process');
function run_in_child (array, cb) {
    var process = child_process.exec ('node libfn.js', function (err, stdout, stderr) {
        var output = JSON.parse (stdout);
        cb (ошибка, вывод);
    });
    process.stdin.write (JSON.stringify (array), 'utf8');
    process.stdin.end ();
}
Дейв Допсон
источник
11
Просто чтобы прояснить ... Узел не однопоточный, а выполнение вашего JavaScript. IO и что не работает на отдельных потоках.
Брэд
3
@Brad - возможно. это зависит от реализации. При соответствующей поддержке ядра интерфейс между Node и ядром может быть основан на событиях - kqueue (mac), epoll (linux), порты завершения ввода-вывода (windows). Как запасной вариант, пул потоков также работает. Ваш основной пункт, хотя верно. Низкоуровневая реализация узла может иметь несколько потоков. Но они НИКОГДА не будут напрямую выставлять их пользователям JS, поскольку это нарушит всю языковую модель.
Дэйв Допсон,
4
Правильно, я просто уточняю, потому что концепция многих сбила с толку.
Брэд
6

Array.forEachПредназначен для ожидающих вычислений, и ничего не получится сделать асинхронными вычисления в цикле событий (веб-работники добавляют многопроцессорность, если вам нужны многоядерные вычисления). Если вы хотите дождаться завершения нескольких задач, используйте счетчик, который можно обернуть в класс семафора.

Tobu
источник
5

Изменить 2018-10-11: Похоже, есть большая вероятность, что описанный ниже стандарт может не пройти, рассмотрите конвейеризацию как альтернативу (не ведет себя точно так же, но методы могут быть реализованы в подобной усадьбе).

Именно поэтому я в восторге от es7, в будущем вы сможете сделать что-то вроде приведенного ниже кода (некоторые спецификации не завершены, поэтому используйте с осторожностью, я постараюсь держать это в курсе). Но в основном используя оператор new :: bind, вы сможете запустить метод для объекта, как если бы прототип объекта содержал метод. например, [Object] :: [Method], где обычно вы вызываете [Object]. [ObjectsMethod]

Обратите внимание, чтобы сделать это сегодня (24 июля-16) и заставить его работать во всех браузерах, вам понадобится перенести ваш код для следующих функций: импорт / экспорт , функции стрелок , обещания , асинхронное / ожидание и, что наиболее важно, связывание функций . Приведенный ниже код может быть модифицирован для использования только функции связывания, если это необходимо, и все эти функциональные возможности сегодня доступны благодаря использованию babel .

YourCode.js (где « много работы » нужно просто вернуть обещание, разрешив его после выполнения асинхронной работы.)

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

Это короткая асинхронная функция для использования без сторонних библиотек

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);
};
Ракс Вунтер
источник
Как это асинхронно? AFAIK # вызов будет выполняться немедленно?
Джайлс Уильямс
1
Конечно, немедленно, но у вас есть функция обратного вызова, чтобы знать, когда все итерации будут завершены. Здесь аргумент «итератор» - это асинхронная функция в стиле узла с обратным вызовом. Это похоже на метод async.each
Ракс Вунтер
3
Я не вижу, как это асинхронно. Позвоните или подайте заявку синхронно. Наличие обратного вызова не делает его асинхронным
adrianvlupu
в javascript, когда люди говорят «асинхронно», они имеют в виду, что выполнение кода не блокирует основной цикл обработки событий (иначе говоря, процесс не застревает в одной строке кода). просто установка обратного вызова не делает код асинхронным, он должен использовать некоторую форму освобождения цикла событий, такую ​​как setTimeout или setInterval. с тех пор, как вы ожидаете их, другой код может работать без прерываний.
Васильевич
0

На npm есть пакет для легкой асинхронности для каждого цикла .

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');
});

Также еще один вариант для AllAsync

Филип Киркбрайд
источник
0

Например, можно закодировать даже такое решение:

 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);
});

С другой стороны, это намного медленнее, чем «для».

В противном случае превосходная библиотека Async может сделать это: https://caolan.github.io/async/docs.html#each

Signo
источник
-1

Вот небольшой пример, который вы можете запустить, чтобы проверить это:

[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);
});

Он будет производить что-то вроде этого (если это займет слишком меньше / много времени, увеличьте / уменьшите количество итераций):

(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
adiian
источник
Это произойдет, даже если вы напишите async.foreach или любой другой параллельный метод. Поскольку цикл for не является процессом ввода-вывода, Nodejs всегда будет делать это синхронно.
Судханшу Гаур
-2

Использование Promise.each из Bluebird библиотеки.

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

Этот метод выполняет итерацию по массиву или обещанию массива, которое содержит обещания (или сочетание обещаний и значений) с заданной функцией итератора с сигнатурой (значение, индекс, длина), где значение является разрешенным значением соответствующее обещание во входном массиве. Итерация происходит поочередно. Если функция итератора возвращает обещание или toableable, то результат обещания ожидается перед продолжением следующей итерации. Если какое-либо обещание во входном массиве отклонено, то возвращенное обещание также отклоняется.

Если все итерации успешно разрешены, Promise.each преобразуется в исходный массив без изменений . Однако, если одна итерация отклоняет или выдает ошибку, Promise.each немедленно прекращает выполнение и не обрабатывает дальнейшие итерации. В этом случае возвращается исходное или ошибочное значение, а не исходный массив.

Этот метод предназначен для побочных эффектов.

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");
});
Игорь Литвинович
источник