JavaScript,Node.js:Array.forEach是异步的吗?

440
我有一个关于 JavaScript 的本地 Array.forEach 实现的问题:它是否表现为异步的? 例如,如果我调用:
[many many elements].forEach(function () {lots of work to do})

这会是非阻塞的吗?


12个回答

442
不,它是阻塞的。请参考算法的规范。(链接)。然而,在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

另一个选择可能是Web Workers


40
如果你正在使用Node.js,也请考虑使用process.nextTick代替setTimeout。 - Marcello Bastea-Forte
33
从技术上讲,forEach不是“阻塞”的,因为CPU从未进入休眠状态。它是同步且CPU绑定的,当您期望Node应用响应事件时,这可能感觉像“阻塞”。 - Dave Dopson
3
在这里,使用async(https://www.npmjs.org/package/async)可能是更合适的解决方案(事实上我刚看到有人把它作为答案发布了!)。 - James
14
我相信这个回答,但在某些情况下似乎是错误的。例如,在使用 forEach 时不会阻塞 await 语句,你应该使用 for 循环代替:http://stackoverflow.com/questions/37962880/why-does-await-not-wait-and-why-does-sequelize-neither-respond-nor-error?noredirect=1#comment63373960_37962880 - Richard
4
当然。你只能在异步函数内使用await。但是forEach不知道什么是异步函数。请记住,异步函数只是返回Promise的函数。你是否期望forEach处理从回调函数返回的Promise?forEach完全忽略回调的返回值。如果回调本身不是异步的,它只能处理非异步回调。 - Felix Kling
显示剩余11条评论

84

如果你需要一个异步友好的Array.forEach等函数,它们可以在Node.js的'async'模块中找到:http://github.com/caolan/async。作为额外收获,这个模块也能在浏览器中使用。

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

3
如果您需要确保异步操作仅以集合中的顺序方式一次处理一个项目,则必须使用eachSeries - matpop
@JohnKennedy 我以前见过你! - Xsmael

18

有一种在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(err, output);
    });
    process.stdin.write(JSON.stringify(array), 'utf8');
    process.stdin.end();
}

13
只是为了明确一点... Node 不是单线程的,但是你的 JavaScript 的执行是单线程的。IO 和其他运行在不同的线程上。 - Brad
4
@Brad - 可能是这样的,具体取决于实现。通过适当的内核支持,Node和内核之间的接口可以基于事件 - kqueue(mac),epoll(linux),IO完成端口(windows)。作为备选方案,线程池也可以工作。你的基本观点是正确的,低级别的Node实现可能有多个线程。但是它们绝对不会直接暴露给JS用户层,因为那会破坏整个语言模型。 - Dave Dopson
4
没错,我只是在澄清一下,因为这个概念让很多人感到困惑。 - Brad
说 Node.js 是单线程的是误导性的。这里有很多技术细节。Javascript 解释器是单线程的,但 IO 子系统(它是 node 的一部分)是多线程的。异步/等待(又名 promises)会调用并行线程。此外,工作线程允许多个 Javascript 线程并行运行。 - pmont

7

Array.forEach 的作用是计算,而不是等待,如果在事件循环中将计算变成异步的话,并没有什么好处(如果需要多核计算,则可以使用WebWorkers来添加多进程)。如果你想要等待多个任务结束,请使用一个计数器,你可以将其包装在一个信号量类中。


5
编辑于2018年10月11日: 看起来下面描述的标准可能无法通过,请考虑pipelineing作为替代方案(行为不完全相同,但方法可以采用类似的方式实现)。
这正是我对es7感到兴奋的原因,在未来,您将能够执行如下代码(某些规范尚未完成,因此请谨慎使用,我会尽力及时更新)。但基本上使用新的::绑定操作符,您将能够像对象的原型包含该方法一样在对象上运行方法。例如[Object] :: [Method],通常情况下,您将调用[Object] . [ObjectsMethod]
请注意,要使其在所有浏览器中正常工作并在今天(2016年7月24日)做到这一点,您将需要为以下功能转换您的代码:导入/导出箭头函数承诺异步/等待和最重要的函数绑定 。如果有必要,下面的代码可以修改为仅使用函数绑定,所有这些功能都可以使用babel轻松获得。 您的Code.js(其中'lots of work to do'必须简单地返回一个promise,在完成异步工作时解析该promise。)
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;
    });
};

2
这些代码片段将帮助您更好地理解forEach和forOf的比较。

/* eslint-disable no-console */
async function forEachTest() {
    console.log('########### Testing forEach ################ ')
    console.log('start of forEachTest func')
    let a = [1, 2, 3]
    await a.forEach(async (v) => {
        console.log('start of forEach: ', v)
        await new Promise(resolve => setTimeout(resolve, v * 1000))
        console.log('end of forEach: ', v)
    })
    console.log('end of forEachTest func')
}
forEachTest()


async function forOfTest() {
    await new Promise(resolve => setTimeout(resolve, 10000)) //just see console in proper way
    console.log('\n\n########### Testing forOf ################ ')
    console.log('start of forOfTest func')
    let a = [1, 2, 3]
    for (const v of a) {
        console.log('start of forOf: ', v)
        await new Promise(resolve => setTimeout(resolve, v * 1000))
        console.log('end of forOf: ', v)
    }
    console.log('end of forOfTest func')
}
forOfTest()


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

1
这个怎么是异步的?据我所知,#call 会立即执行? - Giles Williams
1
当然可以立即执行,但你可以使用回调函数来知道所有迭代何时完成。这里的“iterator”参数是一个带有回调函数的节点式异步函数。它类似于async.each方法。 - Rax Wunter
3
我不认为这是异步的。call或apply是同步的。有回调函数并不意味着它是异步的。 - adrianvlupu
1
在JavaScript中,当人们说异步时,他们的意思是代码执行不会阻塞主事件循环(也就是说,它不会使进程停留在一行代码上)。仅仅放置一个回调函数并不能使代码异步化,它必须利用某种事件循环释放,例如setTimeout或setInterval。因为在等待这些时间期间,其他代码可以无中断地运行。 - vasilevich

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

另一方面,它比"for"要慢得多。

否则,优秀的Async库可以做到这一点:https://caolan.github.io/async/docs.html#each


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

还有另一种变体 forAllAsync


-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

即使您编写了async.foreach或任何其他并行方法,这也将发生。因为对于循环不是IO过程,Nodejs将始终以同步方式执行它。 - Sudhanshu Gaur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接