跳到主要内容

ForEach 控制节点

概述

ForEach控制节点用于循环处理数组中的每个元素,为每个元素创建独立的子工作流环境进行处理。作为纯控制节点,它专注于循环控制逻辑,不进行结果聚合。

节点配置

基本属性

{
"id": "foreach-1",
"type": "control",
"config": {
"identifier": "foreach",
"title": "数组循环处理",
"description": "逐项处理数组数据"
}
}

输入参数

参数名类型默认值描述
dataObject/Array-要处理的数据,可以是数组或包含数组的对象
iterableFieldString'list'从data对象中提取数组的字段名
maxIterationsNumber1000最大迭代次数限制,防止无限循环
continueOnErrorBooleantrue单项失败时是否继续处理其他项

输出端口

端口名描述
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"

数据获取逻辑

  1. 如果data是数组 → 直接迭代
  2. 如果data是对象 → 提取iterableField字段(默认为list
  3. 其他情况 → 使用空数组

迭代上下文

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)来构建累计值或收集结果,并在最后一次迭代时读回。这是行不通的,原因在于引擎运行每次迭代的方式:

  1. 每次迭代获得一份全新的上下文副本。 ForEach 为每次迭代创建新的上下文对象({...context}),因此您在某次迭代中设置的值在下一次迭代中不可见。
  2. 自定义代码节点收到的是只读的上下文快照。 js-executor 操作节点在运行您的代码前会将上下文深度克隆为只读快照,因此代码中对 context.* 的任何赋值都会被丢弃。

因此,那些先修改 context._sumcontext._resultscontext._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 作为纯控制节点,不返回聚合结果。所有的业务逻辑和数据聚合都应该在子工作流中的节点内完成。

相关文档