ForEach 控制节点
概述
ForEach控制节点用于循环处理数组中的每个元素,为每个元素创建独立的子工作流环境进行处理。作为纯控制节点,它专注于循环控制逻辑,不进行结果聚合。
节点配置
基本属性
{
"id": "foreach-1",
"type": "control",
"config": {
"identifier": "foreach",
"title": "数组循环处理",
"description": "逐项处理数组数据"
}
}
输入参数
| 参数名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
data | Object/Array | - | 要处理的数据,可以是数组或包含数组的对象 |
iterableField | String | 'list' | 从data对象中提取数组的字段名 |
maxIterations | Number | 1000 | 最大迭代次数限制,防止无限循环 |
continueOnError | Boolean | true | 单项失败时是否继续处理其他项 |
输出端口
| 端口名 | 描述 |
|---|---|
item | 连接到子工作流,传递当前迭代项 |
数据输入方式
方式1:直接数组
当data本身是数组时,ForEach直接迭代这个数组:
// 数据生成器返回
return [1, 2, 3, 4, 5];
// ForEach会迭代每个数字
方式2:对象中的数组字段
当data是对象时,ForEach从中提取指定字段的数组:
// 数据生成器返回
return {
list: [
{ id: 1, name: "商品A" },
{ id: 2, name: "商品B" }
],
total: 2
};
// ForEach会迭代list字段中的数组
方式3:自定义字段名
通过iterableField参数指定字段名:
// 数据生成器返回
return {
products: [
{ id: 1, name: "商品A" },
{ id: 2, name: "商品B" }
]
};
// 设置iterableField为"products"
数据获取逻辑:
- 如果
data是数组 → 直接迭代 - 如果
data是对象 → 提取iterableField字段(默认为list) - 其他情况 → 使用空数组
迭代上下文
ForEach为每次迭代提供丰富的上下文信息,在子工作流中可以通过context[节点ID]访问:
// 上下文结构
context['foreach-1'] = {
item: currentItem, // 当前迭代项
_iteration_info: {
index: 0, // 当前索引(从0开始)
total: 5, // 总项目数
isFirst: true, // 是否第一项
isLast: false // 是否最后一项
}
}
使用示例
示例1:数值数组处理
{
"nodes": {
"data-generator": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": "return [10, 20, 30, 40, 50];"
}
},
"foreach-numbers": {
"type": "control",
"config": {
"identifier": "foreach"
}
},
"number-processor": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": `
const number = context['foreach-numbers'].item;
const info = context['foreach-numbers']._iteration_info;
console.log(\`处理第\${info.index + 1}个数字: \${number}\`);
return {
original: number,
doubled: number * 2,
isLast: info.isLast
};
`
}
}
},
"edges": [
{
"source": "data-generator:result",
"target": "foreach-numbers:data"
},
{
"source": "foreach-numbers:item",
"target": "number-processor:parameters"
}
]
}
示例2:对象数组处理
{
"nodes": {
"user-generator": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": `
return {
list: [
{ id: 1, name: "张三", age: 25 },
{ id: 2, name: "李四", age: 30 },
{ id: 3, name: "王五", age: 28 }
]
};
`
}
},
"foreach-users": {
"type": "control",
"config": {
"identifier": "foreach"
}
},
"user-processor": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": `
const user = context['foreach-users'].item;
const info = context['foreach-users']._iteration_info;
return {
...user,
processOrder: info.index + 1,
isAdult: user.age >= 18,
isFirstUser: info.isFirst,
isLastUser: info.isLast
};
`
}
}
},
"edges": [
{
"source": "user-generator:result",
"target": "foreach-users:data"
},
{
"source": "foreach-users:item",
"target": "user-processor:parameters"
}
]
}
示例3:在循环之后聚合结果
每次迭代都在各自独立的上下文中运行,无法累加到共享变量中(见下方说明)。正确的做法是:在循环内为每一项返回一个值,然后在循环之后、由循环结束后运行的下游节点进行聚合。
{
"nodes": {
"score-generator": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": "return [85, 92, 78, 96, 88];"
}
},
"foreach-scores": {
"type": "control",
"config": {
"identifier": "foreach"
}
},
"score-processor": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": "const score = context['foreach-scores'].item; return { score: score, passed: score >= 60 };"
}
}
},
"edges": [
{
"source": "score-generator:result",
"target": "foreach-scores:data"
},
{
"source": "foreach-scores:item",
"target": "score-processor:parameters"
}
]
}
在本示例中,score-processor 的每次迭代都为单个项目返回 { score, passed }。ForEach 节点会收集所有迭代中这些每节点的结果。要计算平均值或其他聚合值,请在 ForEach 节点之后添加一个节点来读取收集到的结果并进行归约——循环本身不返回聚合数据。
错误处理
默认行为(continueOnError = true)
// 单项失败时继续处理其他项
const continueOnError = inputs.continueOnError !== false; // 默认为true
if (continueOnError) {
console.warn('迭代失败,继续下一个迭代');
results.push(null); // 失败的迭代用null表示
}
严格模式(continueOnError = false)
// 任何失败都停止整个循环
if (!continueOnError) {
throw error; // 抛出异常,停止执行
}
执行特性
顺序执行
ForEach采用顺序执行模式,逐个处理数组元素:
- 等待当前迭代完成后才开始下一个
- 避免并发资源竞争
- 保证处理顺序
执行指标
ForEach会输出详细的执行统计信息:
// 控制台输出示例
ForEach节点执行完成 {
nodeId: 'foreach-scores',
totalIterations: 5,
successCount: 5,
errorCount: 0,
executionTime: '125ms'
}
安全限制
- 最大迭代次数:默认1000次,防止无限循环
- 数据转换:自动处理非数组数据
null/undefined→ 空数组[]- 对象 →
Object.values(object) - 其他类型 →
[value]
为何无法跨迭代累加
一个常见的错误是:试图通过向上下文上的共享变量写入(例如 context._sum += item)来构建累计值或收集结果,并在最后一次迭代时读回。这是行不通的,原因在于引擎运行每次迭代的方式:
- 每次迭代获得一份全新的上下文副本。 ForEach 为每次迭代创建新的上下文对象(
{...context}),因此您在某次迭代中设置的值在下一次迭代中不可见。 - 自定义代码节点收到的是只读的上下文快照。
js-executor操作节点在运行您的代码前会将上下文深度克隆为只读快照,因此代码中对context.*的任何赋值都会被丢弃。
因此,那些先修改 context._sum、context._results、context._batch 等、再在 isLast 时读回的写法会悄无声息地丢失数据。这也是下方提醒中指出 ForEach 本身不返回聚合结果的原因。
正确做法:在下游聚合
在循环内完成逐项处理并为每一项返回一个值,然后在 ForEach 节点之后的节点中进行累加、过滤或分批:
数据源 → ForEach (item) → 逐项处理节点
│
└──(循环完成)→ 聚合节点
逐项处理节点为每个项目返回一个结果;聚合节点在循环结束后运行一次,将收集到的结果归约为总和、过滤后的列表、批次或您需要的任何形式。请保持循环内代码无状态——仅依赖当前迭代的 item 和 _iteration_info。
最佳实践
1. 数据输入建议
// ✅ 推荐:直接数组(最简单)
return [1, 2, 3, 4, 5];
// ✅ 推荐:结构化对象(更灵活)
return {
list: [1, 2, 3, 4, 5],
metadata: { source: 'api', timestamp: Date.now() }
};
2. 上下文使用技巧
请保持循环内代码无状态——仅读取当前迭代的 item 和 _iteration_info,并返回逐项结果。不要通过写入 context.* 在迭代之间传递数据,这些写入会被丢弃(参见为何无法跨迭代累加)。
// 利用迭代信息(只读)并返回逐项结果
const processor = `
const item = context['foreach-1'].item;
const info = context['foreach-1']._iteration_info;
return {
item: item,
position: info.index + 1,
isFirst: info.isFirst,
isLast: info.isLast
};
`;
3. 错误处理策略
// 根据需求选择错误处理模式
{
"continueOnError": true // 容错模式,适合数据清洗
}
{
"continueOnError": false // 严格模式,适合关键业务
}
ForEach 作为纯控制节点,不返回聚合结果。所有的业务逻辑和数据聚合都应该在子工作流中的节点内完成。
相关文档
- 工作流概述 — 工作流引擎基础
- 上下文管理 — 工作流上下文与数据传递
- If-Else 条件节点 — 条件控制节点
- Switch 分支节点 — 多分支控制节点