Node.js 数组队列应用与 forEach 异步处理详解

作者:夜航船

时间:2026-10-24

浏览:122

本文基于 Node v6.9.5 测试,详解数组利用 push/shift 实现队列的机制,对比 delete 与 splice 删除元素的差异。重点探讨 forEach 在异步场景下的执行顺序问题,通过 Promise.all 实现循环任务完成后的同步回调,并验证大数组下的性能边界。

文章目录

本文主要记录在 Node.js 开发中由数组特性引发的问题及其解决策略,并展示数组的灵活应用方式。

文中所有代码测试均基于 Node.js v6.9.5 版本环境。

数组与队列

通过调用数组对象的 push 和 shift 方法,可以实现队列的先进先出(FIFO)特性,具体示例如下:


>a=[]
[]
>a.push(2.3.4)
3
>a.push(2)
3
>a
[2.3.4.2]
>a.shift()
2
>a
>[3.4.2]

数组与 forEach

数组中删除元素主要有两种方式:使用 delete 运算符或使用 splice 方法,二者存在显著区别。

操作/方法 说明
splice 删除并返回指定的数组元素,数组本身长度会改变;但不会free元素对象
delete 删除(free)元素对象,数组元素不变,值变为undefined

若需从数组中彻底移除某个元素,应使用 splice 方法:


> a=[1,2,3]
[ 1, 2, 3 ]
> a.splice(1,1)
[ 2 ]
> a
[ 1, 3 ]
> a.length
2
> a.forEach(function(item, index){console.info("index[", index,"]:", item)});
index[ 0 ]: 1
index[ 1 ]: 3
undefined
> 

那么,在使用 delete 删除元素后,执行 forEach 会产生什么效果?

forEach 对包含空元素的数组处理机制

测试结果如下所示:


> a=[1,2,3]
[ 1, 2, 3 ]
> delete a[1]
true
> a
[ 1, , 3 ]
> a.length
3
> a.forEach(function(item, index){console.info("index[", index,"]:", item)});
index[ 0 ]: 1
index[ 2 ]: 3
undefined

从测试结果可见,forEach 不会遍历值为 undefined 的空位。这在实际应用中,如何判断 forEach 是否真正结束成为一个挑战。

为解决配合 forEach 的异步特性应用,可以通过为数组添加 prototype 属性来自行管理有效数据:

效果如下所示:


> a=[1,2,3]
[ 1, 2, 3 ]
> a.validnum=3
3
> delete a[2]
true
> a.validnum=2
2
> a
[ 1, 2, , validnum: 2 ]
> a.length
3
> a.validnum
2
> a.forEach(function(item, index){console.info("index[", index,"]:", item)});
index[ 0 ]: 1
index[ 1 ]: 2
undefined
> 

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

习惯于 C 语言系的编程思维,初次接触 Node.js 时,其异步处理机制曾带来困扰。

编码时遇到一个场景:需要循环处理数组中的每个元素,并在全部处理完成后执行一个 last 操作。但由于 JS 的异步特性,last 语句往往先于循环任务执行,因此需要深入研究 forEach 的行为。

Talk is cheap. Show me the code.

forEach 用法

forEach 用于遍历数组结构。尽管有人称其底层由 for 循环实现,但效果上二者一致。forEach 的回调函数接收三个参数:值、索引(从 0 开始)和原数组。


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

输出结果


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

输出结果


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

输出结果


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

输出结果


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 上下文语句同步处理。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持本站。如有错误或未考虑完全的地方,望不吝赐教。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章