Node.js 数组 forEach 同步处理上下文语句

习惯了C语言系的思维方式,刚接触Node.js,它的异步处理让我头大。
写代码遇到这么一个场景,需要循环对一个数组中的元素进行处理,全部处理完成后再执行一个last操作。但是JS的异步特性会使这个last语句先执行,所以花点时间研究研究forEach。
Talk is cheap. Show me the code.

forEach 用法

forEach用于对数组结构进行遍历,看到有人说forEach底层是用for实现的,没深究,起码效果上看是一样的。forEach的回调函数3个参数分别是:值、序号和原数组。序号从0开始。

(() => {
    let arr = [2, 3, 1];
    arr.forEach(function (value, index, array) {
        console.log(value);
        console.log(index);
        console.log(array);
        console.log('-----');
    });
})();

Output

2
0
[ 2, 3, 1 ]
-----
3
1
[ 2, 3, 1 ]
-----
1
2
[ 2, 3, 1 ]
-----

从结果上看forEach多次循环之间是同步的,也就是说都是按顺序执行的。但是一想到它是JS就感觉不可能同步的。。可以验证一下。

forEach 异步处理多次循环

这次在forEach加个定时任务,每次循环操作都延时value相关的时间,模拟比较耗时的操作。

(() => {
    let arr = [2, 3, 1];
    arr.forEach(function (value, index, array) {
        setTimeout(function () {
            console.log(value);
        }, value*100);
    });
})();

Output

1
2
3

从结果可以看出耗时最短的任务先完成,每次循环的任务并不是按循环的先后顺序执行的,也就是说异步处理多次循环。

forEach 上下文也是异步执行

回到开始说到的问题了,且不管多次循环是不是按顺序执行,我需要forEach中的所有任务都完成后执行一条数据来通知我任务全部完成了。

(() => {
    let arr = [2, 3, 1];
    arr.forEach(function (value, index, array) {
        setTimeout(function () {
            console.log(value);
        }, value*100);
    });
    console.log('All the work is done');
})();

Output

All the work is done
1
2
3

从结果来看,上下文的语句也不是同步的,forEach循环中的任务没有完成就通知所有任务都完成了,显然不符合预期。
针对这个问题看了好多个博客,都没有找到合适的解决方法,最后只能想到用Promise.all来勉强实现这个功能。

Promise.all 实现 forEach 上下文语句同步处理

把上面的代码改成Promise.all的结构。每个循环中执行结束调用resolve(),我们知道Promise.all的then函数,只有所有的Promise都执行完成才会触发,这样好像能满足我们的需求。

(() => {
    let arr = [2, 3, 1];
    let proArr = [];
    arr.forEach(function (value, index) {
        proArr[index] = new Promise(function (resolve) {
            setTimeout(function () {
                console.log(value);
                resolve();
            }, value*100);
        });
    });

    Promise.all(proArr).then(()=>{
        console.log('All the work is done');
    })
})();

Output

1
2
3
All the work is done

从结果来看,满足了我们的需求。

可能还存在的问题

想到JS异步特性,突然发现可能这个方法还存在个问题。
这里每次 forEach 刚进入就对 Promise 数组进行了赋值操作,这个操作时间应该非常短,循环3次都赋值完成后才调用最后的Promise.all语句。
但是如果这个数组非常大,这个循环赋值的操作非常耗时间的话,假如只完成了一半的赋值操作,那么执行最后这个 Promise.all 的时候传入的 Promise 数组可能并不是包含所有 Promise 的数组。
这样的话 Promise.all 等待的就只有一半的操作,Promise.all 等待的时候,这个数组后面被赋值的 Promise 不知道会不会被等待。
刚接触JS不明白实现机制,只能实验来验证一下是否存在这个问题。接下来用把这个数组弄大一些,请原谅我用最傻瓜式的方式搞大它。

(() => {
    let arr = [2, 3, 1, 2, 3, 1, 2, 3, 1, 2];   // 10
    arr= arr.concat(arr);   // 2^1 * 10
    arr= arr.concat(arr);   // 2^2 * 10
    arr= arr.concat(arr);   // 2^3
    arr= arr.concat(arr);   // 2^4
    arr= arr.concat(arr);   // 2^5
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);   // 2^10
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);
    arr= arr.concat(arr);   // 2^15
    arr= arr.concat(arr);
    arr= arr.concat(arr);	// 2^17 * 10
//  arr= arr.concat(arr);   // 2^18 * 10

    console.log(arr.length);
    let proArr = [];
    arr.forEach(function (value, index) {
        proArr[index] = new Promise(function (resolve) {
            setTimeout(function () {
                console.log(value);
                resolve();
            }, value*100);
        });
    });

    Promise.all(proArr).then(()=>{
        console.log('All the work is done');
        console.log(arr.length);
    }).catch(function (err) {
        console.log(err);
    })
})();

经过测试在我这个电脑上当数组长度为2^18 * 10的时候,Promise报错 RangeError: Too many elements passed to Promise.all
当数组长度为2^17 * 10 即2621440的时候,会正常运行。测试了几次,最后的执行命令输出的All the work is done始终在最后输出(因为终端缓冲区太小,所以使用node xx.js > log.txt重定向的方式把输出结果重定向到文件查看)。当然应用中也不会有这么大的数组,从结果看的话,就是实际应用中不存在上面考虑可能出现的问题。
也就是说可以用 Promise.all 实现 forEach 上下文语句同步处理。
如果有对JS理解透彻的朋友看出了上文的问题,欢迎指教。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值