JavaScript, Node.js: là Array.forEach không đồng bộ?


378

Tôi có một câu hỏi liên quan đến việc Array.forEachtriển khai JavaScript nguyên gốc : Nó có hoạt động không đồng bộ không? Ví dụ: nếu tôi gọi:

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

Điều này sẽ không chặn?


Câu trả lời:


392

Không, nó đang chặn. Có một cái nhìn vào các đặc điểm kỹ thuật của thuật toán .

Tuy nhiên, việc triển khai có thể dễ hiểu hơn được đưa ra trên 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);
    }
  };
}

Nếu bạn phải thực thi nhiều mã cho mỗi phần tử, bạn nên xem xét sử dụng một cách tiếp cận khác:

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

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

và sau đó gọi nó bằng:

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

Điều này sẽ không chặn sau đó. Ví dụ được lấy từ JavaScript hiệu suất cao .

Một lựa chọn khác có thể là nhân viên web .


37
Nếu bạn đang sử dụng Node.js, hãy cân nhắc sử dụng process.nextTick thay vì setTimeout
Marcello Bastea-Forte

28
về mặt kỹ thuật, forEach không "chặn", vì CPU không bao giờ đi ngủ. Nó đồng bộ và bị ràng buộc bởi CPU, có thể có cảm giác như "chặn" khi bạn mong muốn ứng dụng nút phản ứng nhanh với các sự kiện.
Dave Dopson

3
async có lẽ sẽ là một giải pháp thích hợp hơn ở đây (thực tế chỉ cần thấy ai đó đăng nó như một câu trả lời!).
James

6
Tôi tin tưởng câu trả lời này, nhưng nó có vẻ sai trong một số trường hợp. forEachkhông không chặn trên awaitbáo cáo ví dụ và bạn thay vì phải sử dụng một forvòng lặp: stackoverflow.com/questions/37962880/...
Richard

3
@Richard: tất nhiên rồi. Bạn chỉ có thể sử dụng các chức năng awaitbên trong async. Nhưng forEachkhông biết chức năng async là gì. Hãy nhớ rằng các chức năng không đồng bộ chỉ là các chức năng trả lại một lời hứa. Bạn có muốn forEachxử lý một lời hứa được trả lại từ cuộc gọi lại không? forEachhoàn toàn bỏ qua giá trị trả về từ cuộc gọi lại. Nó chỉ có thể xử lý một cuộc gọi lại async nếu chính nó là async.
Felix Kling

80

Nếu bạn cần một phiên bản thân thiện Array.forEachvà không đồng bộ , tương tự, chúng có sẵn trong mô-đun 'async' của Node.js: http://github.com/caolan/async ... như một phần thưởng mà mô-đun này cũng hoạt động trong trình duyệt .

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

2
Nếu bạn cần đảm bảo rằng opeartion opync chỉ được chạy cho một mục tại một thời điểm (theo thứ tự của bộ sưu tập) , bạn phải sử dụng eachSeriesthay thế.
matpop

@JohnKennedy Tôi đã thấy bạn trước đây!
Xsmael

16

Có một mô hình chung để thực hiện một tính toán thực sự nặng nề trong Node có thể áp dụng cho bạn ...

Nút là một luồng đơn (như một lựa chọn thiết kế có chủ ý, xem Node.js là gì? ); điều này có nghĩa là nó chỉ có thể sử dụng một lõi duy nhất. Các hộp hiện đại có 8, 16 hoặc thậm chí nhiều lõi hơn, do đó, điều này có thể khiến 90 +% máy không hoạt động. Mẫu chung cho dịch vụ REST là khởi động một quy trình nút trên mỗi lõi và đặt chúng sau bộ cân bằng tải cục bộ như http://nginx.org/ .

Ngã ba một đứa trẻ - Đối với những gì bạn đang cố gắng làm, có một mô hình phổ biến khác, từ bỏ một quá trình trẻ em để thực hiện việc nâng vật nặng. Ưu điểm là quy trình con có thể thực hiện tính toán nặng nề trong nền trong khi quy trình cha mẹ của bạn phản ứng nhanh với các sự kiện khác. Điều hấp dẫn là bạn không thể / không nên chia sẻ bộ nhớ với quy trình con này (không phải không có nhiều mâu thuẫn và một số mã riêng); bạn phải truyền tin nhắn Điều này sẽ hoạt động tốt nếu kích thước của dữ liệu đầu vào và đầu ra của bạn nhỏ so với tính toán phải được thực hiện. Bạn thậm chí có thể khởi động một tiến trình node.js con và sử dụng cùng mã mà bạn đang sử dụng trước đó.

Ví dụ:

var child_ process = Yêu cầu ('child_ process');
hàm run_in_child (mảng, cb) {
    var process = child_ process.exec ('nút libfn.js', function (err, stdout, stderr) {
        var output = JSON.parse (thiết bị xuất chuẩn);
        cb (err, đầu ra);
    });
    process.stdin.write (JSON.opesify (mảng), 'utf8');
    process.stdin.end ();
}

11
Để rõ ràng ... Nút không phải là một luồng đơn, nhưng việc thực thi JavaScript của bạn là. IO và những gì không chạy trên các chủ đề riêng biệt.
Brad

3
@Brad - có thể. đó là phụ thuộc thực hiện. Với sự hỗ trợ kernel thích hợp, giao diện giữa Node và kernel có thể dựa trên sự kiện - kqueue (mac), epoll (linux), cổng hoàn thành IO (windows). Là một dự phòng, một nhóm các chủ đề cũng hoạt động. Điểm cơ bản của bạn là đúng mặc dù. Việc thực hiện Node cấp thấp có thể có nhiều luồng. Nhưng họ sẽ KHÔNG BAO GIỜ trực tiếp đưa họ đến vùng người dùng JS vì điều đó sẽ phá vỡ toàn bộ mô hình ngôn ngữ.
Dave Dopson

4
Đúng, tôi chỉ làm rõ vì khái niệm này đã làm nhiều người bối rối.
Brad

6

Array.forEachcó nghĩa là để các công cụ tính toán không chờ đợi và không có gì đạt được khi tính toán không đồng bộ trong một vòng lặp sự kiện (webworkers thêm đa xử lý, nếu bạn cần tính toán đa lõi). Nếu bạn muốn đợi nhiều tác vụ kết thúc, hãy sử dụng một bộ đếm mà bạn có thể gói trong một lớp semaphore.


5

Chỉnh sửa 2018-10-11: Có vẻ như rất có thể tiêu chuẩn được mô tả dưới đây có thể không được thông qua, hãy xem xét đường ống như một giải pháp thay thế (không hành xử giống hệt nhau nhưng các phương pháp có thể được thực hiện theo cách tương tự).

Đây chính xác là lý do tại sao tôi rất hào hứng với es7, trong tương lai bạn sẽ có thể làm một cái gì đó giống như mã bên dưới (một số thông số kỹ thuật chưa hoàn tất nên hãy thận trọng, tôi sẽ cố gắng cập nhật thông tin này). Nhưng về cơ bản bằng cách sử dụng toán tử :: bind mới, bạn sẽ có thể chạy một phương thức trên một đối tượng như thể nguyên mẫu của đối tượng có chứa phương thức đó. ví dụ: [Object] :: [Phương thức] trong đó thông thường bạn sẽ gọi [Object]. [ObjectMethod]

Lưu ý để thực hiện việc này ngay hôm nay (24-ngày 16 tháng 7) và để nó hoạt động trong tất cả các trình duyệt, bạn sẽ cần phải dịch mã cho các chức năng sau: Nhập / Xuất , Hàm mũi tên , Lời hứa , Async / Await và quan trọng nhất là liên kết chức năng . Mã dưới đây có thể được sửa đổi để chỉ sử dụng chức năng liên kết nếu không cần thiết, tất cả chức năng này có sẵn gọn gàng ngày hôm nay bằng cách sử dụng babel .

YourCode.js (trong đó ' rất nhiều việc phải làm ' chỉ cần trả lại một lời hứa, giải quyết nó khi công việc không đồng bộ được thực hiện.)

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

Đây là một chức năng không đồng bộ ngắn để sử dụng mà không yêu cầu lib của bên thứ ba

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

Làm thế nào là không đồng bộ này? AFAIK #call sẽ thực thi ngay lập tức?
Giles Williams

1
Tất nhiên ngay lập tức, nhưng bạn có chức năng gọi lại để biết khi nào tất cả các lần lặp được hoàn thành. Ở đây, đối số "iterator" là một hàm async kiểu nút với gọi lại. Nó tương tự như phương thức async.each
Rax Wunter 29/07/2015

3
Tôi không thấy làm thế nào là không đồng bộ. gọi hoặc áp dụng là đồng bộ. Có một cuộc gọi lại không làm cho nó không đồng bộ
adrianvlupu

trong javascript khi mọi người nói async, điều đó có nghĩa là việc thực thi mã không chặn vòng lặp sự kiện chính (hay còn gọi là nó không làm cho bộ xử lý bị kẹt ở một dòng mã). chỉ cần đặt một cuộc gọi lại không tạo ra mã không đồng bộ, nó phải sử dụng một số hình thức phát hành vòng lặp sự kiện như setTimeout hoặc setInterval. kể từ khi kéo dài thời gian bạn chờ đợi, các mã khác có thể chạy mà không bị gián đoạn.
vasilevich

0

Có một gói trên npm để dễ dàng không đồng bộ cho mỗi vòng lặp .

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

Ngoài ra một biến thể khác cho ALLAsync


0

Có thể mã ngay cả giải pháp như thế này chẳng hạn:

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

Mặt khác, nó chậm hơn nhiều so với "cho".

Mặt khác, thư viện Async tuyệt vời có thể làm điều này: https://caolan.github.io/async/docs.html#each


0

Đây là một ví dụ nhỏ bạn có thể chạy để kiểm tra nó:

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

Nó sẽ tạo ra một cái gì đó như thế này (nếu mất quá ít / nhiều thời gian, tăng / giảm số lần lặp):

(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

Điều này sẽ xảy ra ngay cả khi bạn sẽ viết async.foreach hoặc bất kỳ phương thức song song nào khác. Bởi vì vòng lặp for không phải là một quá trình IO, Nodejs sẽ luôn thực hiện đồng bộ.
Bò tót Sudhanshu

-2

Sử dụng Promise.each của thư viện bluebird .

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

Phương thức này lặp lại trên một mảng hoặc một lời hứa của một mảng chứa các lời hứa (hoặc kết hợp các lời hứa và giá trị) với hàm lặp được đưa ra với chữ ký (giá trị, chỉ mục, độ dài) trong đó giá trịgiá trị được giải quyết của một lời hứa tương ứng trong mảng đầu vào. Lặp lại xảy ra ser seri. Nếu chức năng lặp trả về một lời hứa hoặc có thể sau đó, thì kết quả của lời hứa được chờ đợi trước khi tiếp tục với lần lặp tiếp theo. Nếu bất kỳ lời hứa nào trong mảng đầu vào bị từ chối, thì lời hứa trả lại cũng bị từ chối.

Nếu tất cả các lần lặp giải quyết thành công, Promise.each sẽ giải quyết mảng ban đầu không được sửa đổi . Tuy nhiên, nếu một lần lặp từ chối hoặc lỗi, Promise.each ngừng thực thi ngay lập tức và không xử lý bất kỳ lần lặp tiếp theo nào. Lỗi hoặc giá trị bị từ chối được trả về trong trường hợp này thay vì mảng ban đầu.

Phương pháp này có nghĩa là được sử dụng cho các tác dụng phụ.

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");
});
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.