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
@ Річард: звичайно. Можна використовувати лише 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
});

2
Якщо вам потрібно переконатися, що операція асинхронізації виконується лише для одного предмета одночасно (у порядку колекції) , ви повинні використовувати eachSeriesзамість цього.
матпоп

@JohnKennedy Я бачив тебе раніше!
Xsmael

16

Існує загальна модель для виконання дійсно важких обчислень в Node, яка може бути застосовна для вас ...

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

Розсипання дитини - Для того, що ви намагаєтеся зробити, існує ще одна поширена модель, яка змушує дитину робити важкі підйоми. Перевага полягає в тому, що дочірній процес може робити важкі обчислення у фоновому режимі, поки ваш батьківський процес реагує на інші події. Проблема полягає в тому, що ви не можете / не повинні ділитися пам’яттю з цим дочірнім процесом (не маючи багато контурів і деякого нативного коду); ви повинні передавати повідомлення. Це буде добре працювати, якщо розмір вхідних та вихідних даних невеликий порівняно з обчисленнями, які необхідно виконати. Ви навіть можете запустити дочірній процес node.js і використовувати той самий код, який ви використовували раніше.

Наприклад:

var child_process = вимагати ('child_process');
функція run_in_child (масив, cb) {
    var process = child_process.exec ('вузол libfn.js', функція (помилка, stdout, stderr) {
        var вихід = JSON.parse (stdout);
        cb (помилка, вихід);
    });
    process.stdin.write (JSON.stringify (масив), 'utf8');
    process.stdin.end ();
}

11
Просто щоб було зрозуміло ... Вузол не є однопоточним, але виконання вашого JavaScript є. IO і те, що не працює на окремих потоках.
Бред

3
@Brad - можливо. це залежить від реалізації. При відповідній підтримці ядра інтерфейс між Node та ядром може бути заснований на подіях - kqueue (mac), epoll (linux), порти завершення IO (windows). Як запасний варіант, також працює пул ниток. Ваша основна точка, правда, правда. Реалізація вузла низького рівня може мати декілька потоків. Але вони НІКОЛИ безпосередньо не піддаватимуть їх користувачеві JS, оскільки це порушить всю мовну модель.
Дейв Допсон

4
Правильно, я лише уточнюю, тому що концепція багатьох бентежила.
Бред

6

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


5

Редагувати 2018-10-11: Схоже, є хороший шанс, що описаний нижче стандарт може не пройти, розгляньте трубопровід як альтернативу (не так само, але методи можуть бути реалізовані в подібній садибі).

Саме тому я схвильований щодо es7, в майбутньому ви зможете зробити щось на зразок коду нижче (деякі технічні характеристики не завершені, тому використовуйте з обережністю, я постараюсь бути в курсі цього). Але в основному, використовуючи новий :: bind-оператор, ви зможете запустити метод на об’єкт так, ніби прототип об'єкта містить метод. наприклад, [Object] :: [Метод], де зазвичай ви б назвали [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 #call буде виконаний негайно?
Giles Williams

1
Звичайно негайно, але у вас є функція зворотного дзвінка, щоб знати, коли всі ітерації будуть виконані. Тут аргумент "ітератор" - це функція асинхронізації у стилі вузла із зворотним дзвінком. Це схоже на метод async.each
Rax Wunter

3
Я не бачу, як це асинхроніка. дзвінок або заявка є синхронними.
Зворотний

у JavaScript, коли люди говорять про асинхронізацію, вони означають, що виконання коду не блокує основний цикл подій (він же не робить процес, що застряг в одному рядку коду). просто повернення виклику не робить асинхронний код, він повинен використовувати певну форму звільнення циклу подій, наприклад setTimeout або setInterval. оскільки протягом часу, коли ви чекаєте їх, інший код може працювати без перебоїв.
vasilevich

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


0

Ось невеликий приклад, який можна запустити для тестування:

[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

Це станеться, навіть якщо ви напишете async.foreach або будь-який інший паралельний метод. Оскільки цикл не є процесом вводу-виводу, то Nodejs завжди буде робити це синхронно.
Sudhanshu Gaur

-2

Використовуйте Promise.each з бібліотеки синіх птахів .

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

Цей метод повторює масив або обіцянку масиву, який містить обіцянки (або поєднання обіцянок та значень) із заданою функцією ітератора з підписом (значення, індекс, довжина), де значенням є розв'язане значення а відповідна обіцянка у вхідному масиві. Ітерація відбувається серійно. Якщо функція ітератора повертає обіцянку або відверто, результат очікування очікується, перш ніж продовжувати наступну ітерацію. Якщо будь-яка обіцянка у вхідному масиві відхилена, то повернута обіцянка також буде відхилена.

Якщо всі ітерації успішно вирішені , 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");
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.