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

นี่จะไม่ใช่การปิดกั้นในตอนนั้น ตัวอย่างที่จะนำมาจากHigh Performance JavaScript

อีกตัวเลือกหนึ่งอาจจะมีคนงานเว็บ


37
หากคุณใช้ Node.js ให้ลองใช้process.nextTickแทน setTimeout
Marcello Bastea-Forte

28
ในทางเทคนิค forEach ไม่ใช่ "บล็อก" เนื่องจาก CPU จะไม่เข้าสู่โหมดสลีป มันเป็นแบบซิงโครนัสและมี CPU ผูกไว้ซึ่งสามารถรู้สึกเหมือน "บล็อก" เมื่อคุณคาดหวังว่าแอปโหนดจะตอบสนองต่อเหตุการณ์
Dave Dopson

3
asyncน่าจะเป็นทางออกที่เหมาะสมกว่าที่นี่ (อันที่จริงเพิ่งเห็นบางคนโพสต์ว่าเป็นคำตอบ!)
James

6
ฉันเชื่อคำตอบนี้ แต่ดูเหมือนว่าผิดในบางกรณี forEachไม่ได้บล็อกบนawaitข้อความสั่งต่างๆและคุณควรใช้การforวนซ้ำ: stackoverflow.com/questions/37962880/…
Richard

3
@ Richard: แน่นอน คุณสามารถใช้ฟังก์ชั่นawaitภายในasyncเท่านั้น แต่forEachไม่รู้ว่าฟังก์ชั่น async คืออะไร โปรดทราบว่าฟังก์ชั่น async เป็นเพียงฟังก์ชั่นการคืนสัญญา คุณคาดหวังว่าforEachจะได้รับสัญญาที่ส่งคืนจากการติดต่อกลับหรือไม่ forEachละเว้นค่าส่งคืนจากการเรียกกลับทั้งหมด มันจะสามารถจัดการกับการโทรกลับ async ได้หากมันเป็น async เท่านั้น
เฟลิกซ์คลิง

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
หากคุณต้องการตรวจสอบให้แน่ใจว่าการเรียกใช้ async opeartion ทำงานครั้งละหนึ่งไอเท็มเท่านั้น (ตามลำดับการรวบรวม)คุณต้องใช้eachSeriesแทน
matpop

@JohnKennedy ฉันเคยเห็นคุณมาก่อน!
Xsmael

16

มีรูปแบบทั่วไปสำหรับทำการคำนวณที่หนักมากในโหนดที่อาจใช้กับคุณ ...

โหนดเป็นเธรดเดี่ยว (เป็นตัวเลือกการออกแบบโดยเจตนาดูที่ Node.js คืออะไร ) นี่หมายความว่ามันสามารถใช้ประโยชน์จากแกนเดียวเท่านั้น กล่องโมเดิร์นมี 8, 16 หรือคอร์ที่มากขึ้นดังนั้นสิ่งนี้อาจทำให้ 90% ของเครื่องว่าง รูปแบบที่พบบ่อยสำหรับการให้บริการส่วนที่เหลือคือการยิงขึ้นกระบวนการโหนดหนึ่งต่อคอและใส่เหล่านี้อยู่เบื้องหลัง balancer โหลดท้องถิ่นเช่นhttp://nginx.org/

การแยกเด็ก - สำหรับสิ่งที่คุณพยายามจะทำมีรูปแบบทั่วไปอีกอย่างหนึ่งคือการแยกกระบวนการของเด็กออกเพื่อยกของหนัก ข้อดีคือกระบวนการของเด็กสามารถทำการคำนวณอย่างหนักในพื้นหลังในขณะที่กระบวนการหลักของคุณตอบสนองต่อเหตุการณ์อื่น ๆ สิ่งที่จับได้คือคุณไม่สามารถ / ไม่ควรแชร์หน่วยความจำกับกระบวนการลูกนี้ (ไม่ใช่โดยไม่มีการคัดค้านและโค้ดเนทีฟจำนวนมาก); คุณต้องส่งข้อความ สิ่งนี้จะทำงานได้อย่างสวยงามหากขนาดของข้อมูลอินพุตและเอาต์พุตของคุณมีขนาดเล็กเมื่อเทียบกับการคำนวณที่ต้องดำเนินการ คุณสามารถเริ่มกระบวนการ child node.js และใช้รหัสเดียวกันกับที่คุณใช้ก่อนหน้านี้

ตัวอย่างเช่น:

var child_process = require ('child_process');
ฟังก์ชัน run_in_child (อาร์เรย์, cb) {
    var process = child_process.exec ('node libfn.js', ฟังก์ชัน (err, stdout, stderr) {
        var output = JSON.parse (stdout);
        cb (ข้อผิดพลาดเอาต์พุต);
    });
    process.stdin.write (JSON.stringify (อาร์เรย์), 'utf8');
    process.stdin.end ();
}

11
เพื่อให้ชัดเจน ... โหนดไม่ใช่เธรดเดี่ยว แต่การดำเนินการของ JavaScript ของคุณคือ IO และสิ่งที่ไม่ทำงานบนเธรดแยกกัน
แบรด

3
@Brad - อาจจะ ที่ขึ้นอยู่กับการใช้งาน ด้วยการสนับสนุนเคอร์เนลที่เหมาะสมอินเตอร์เฟสระหว่างโหนดและเคอร์เนลสามารถเป็นแบบเหตุการณ์ - kqueue (mac), epoll (linux), พอร์ต IO IO (windows) ในฐานะทางเลือกกลุ่มของเธรดยังใช้งานได้ ประเด็นพื้นฐานของคุณถูกต้องแล้ว การนำโหนดระดับต่ำมาใช้อาจมีหลายเธรด แต่พวกเขาจะไม่เปิดเผยโดยตรงกับผู้ใช้ JS ว่าจะทำลายรูปแบบภาษาทั้งหมด
เดฟ Dopson

4
ถูกต้องฉันแค่ชี้แจงเพราะแนวคิดมีความสับสนมากมาย
แบรด

6

Array.forEachมีไว้สำหรับการคำนวณสิ่งต่าง ๆ ที่ไม่รอและไม่มีอะไรที่จะได้รับการคำนวณแบบอะซิงโครนัสในลูปเหตุการณ์ (webworkers เพิ่มการประมวลผลแบบมัลติหากคุณต้องการการคำนวณแบบมัลติคอร์) หากคุณต้องการรอให้งานหลาย ๆ เสร็จสิ้นให้ใช้ตัวนับซึ่งคุณสามารถใส่ในคลาสเซมาฟอร์ได้


5

แก้ไข 2018/10/11: ดูเหมือนว่ามีโอกาสที่ดีมาตรฐานที่อธิบายด้านล่างอาจจะไม่ผ่านไปพิจารณาpipelineingเป็นทางเลือกอื่น (ไม่ทำงานเหมือนกันทุกประการ แต่อาจใช้วิธีการในคฤหาสน์แบบเดียวกัน)

นี่คือเหตุผลที่ฉันตื่นเต้นกับ es7 ในอนาคตคุณจะสามารถทำบางอย่างเช่นรหัสด้านล่าง (รายละเอียดบางอย่างไม่สมบูรณ์ดังนั้นใช้ด้วยความระมัดระวังฉันจะพยายามทำให้ทันสมัยอยู่เสมอ) แต่โดยพื้นฐานแล้วใช้ตัวดำเนินการ :: ผูกใหม่คุณจะสามารถเรียกใช้เมธอดบนวัตถุราวกับว่าต้นแบบของวัตถุมีวิธีการ เช่น [Object] :: [Method] โดยปกติแล้วคุณจะเรียก [Object] [ObjectsMethod]

หมายเหตุทำวันนี้ (24 ก.ค. 16) และมีมันทำงานในเบราว์เซอร์ทั้งหมดที่คุณจะต้อง transpile รหัสของคุณสำหรับการทำงานต่อไปนี้: นำเข้า / ส่งออก , ฟังก์ชั่นลูกศร , สัญญา , Async / รอและที่สำคัญที่สุดฟังก์ชั่นการผูก โค้ดด้านล่างสามารถ modfied ที่จะใช้เพียงผูกฟังก์ชั่นถ้า nessesary ทุกฟังก์ชั่นนี้สามารถใช้ได้อย่างเรียบร้อยในวันนี้โดยใช้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

นี่เป็นฟังก์ชั่นแบบอะซิงโครนัสสั้น ๆ ที่ใช้โดยไม่ต้องใช้ libs ของบุคคลที่สาม

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
แน่นอนทันที แต่คุณมีฟังก์ชั่นการโทรกลับที่จะรู้ว่าเมื่อการทำซ้ำทั้งหมดจะเสร็จสมบูรณ์ นี่อาร์กิวเมนต์ "iterator" เป็นฟังก์ชั่น async สไตล์โหนดที่มีการโทรกลับ มันคล้ายกับวิธี async.each
Rax Wunter

3
ฉันไม่เห็นว่ามันเป็นแบบอะซิงโครนัสอย่างไร โทรหรือสมัครเป็นแบบซิงโครนัส การโทรกลับไม่ได้ทำให้เหมือนกันเลย
adrianvlupu

ใน javascript เมื่อมีคนพูด async พวกเขาหมายความว่าการเรียกใช้โค้ดไม่ได้ปิดกั้นลูปเหตุการณ์หลัก (อาคาไม่ทำให้ proccess ติดอยู่ที่โค้ดหนึ่งบรรทัด) เพียงแค่วางโทรกลับไม่ได้ทำรหัส async มันจะต้องใช้รูปแบบของวงเหตุการณ์ปล่อยเช่น 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 หรือวิธีขนานอื่น ๆ เพราะสำหรับลูปไม่ใช่กระบวนการ IO Nodejs จะทำแบบซิงโครนัสเสมอ
Sudhanshu Gaur

-2

ใช้Promise.eachของไลบรารีBluebird

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

วิธีนี้วนซ้ำกว่าอาร์เรย์หรือสัญญาของอาร์เรย์ซึ่งมีสัญญา (หรือการผสมผสานของสัญญาและค่า) กับฟังก์ชั่นiterator ที่กำหนดด้วยลายเซ็น(ค่าดัชนีความยาว)โดยที่ค่าเป็นค่าที่ได้รับการแก้ไขของ สัญญาที่เกี่ยวข้องในอาร์เรย์อินพุต การวนซ้ำเกิดขึ้นอย่างเป็นลำดับหากฟังก์ชั่นตัววนซ้ำส่งคืนสัญญาหรือหลังจากนั้นผลลัพธ์ของสัญญาจะถูกรอก่อนดำเนินการต่อด้วยการทำซ้ำครั้งถัดไป หากสัญญาใด ๆ ในอาร์เรย์อินพุตถูกปฏิเสธดังนั้นสัญญาที่ส่งคืนจะถูกปฏิเสธเช่นกัน

หากทั้งหมดของการทำซ้ำแก้ไขเรียบร้อยแล้ว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");
});
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.