一、从孤岛到河流:yield* 如何重塑低代码引擎中的数据流
先说一个核心观点:yield*(Ja vaScript 中是这个写法,Python 里对应的是 yield from)在低代码引擎中实现可视化业务算子的链式流级联,其本质并非仅仅是“添加一种语法糖”那么简单。它解决的关键问题是——当用户拖拽出“查询订单 → 过滤异常 → 计算折扣 → 推送通知”这样的节点序列时,如何让整条执行链像一条顺畅流动的河流,而非多个彼此孤立的水闸。简而言之,就是将数据流的暂停、恢复、委托等能力,映射到可视化节点的执行生命周期中。

业务算子本质上是可中断的数据处理函数
在运行时引擎中,每个可视化算子——例如“HTTP 请求”“条件分支”“JSON 转换”——都不应被编译为那种“一次性完成”的同步阻塞调用。更优的做法是将其建模为一个返回 AsyncGenerator(JS 中)或 async def + yield from(Python 中)的函数。举例来说:
- “分页拉取用户列表”算子:每次
yield一页数据,绝不一次性加载全部数据; - “实时风控校验”算子:对每条流入的事件
yield一个校验结果,天然支持背压机制; - “聚合统计”算子:内部维护状态,先
yield中间聚合值,最后用return返回最终状态。
这样一来,算子自身便具备了“流感知”能力,完全无需额外添加适配层。这才是从根源上解决问题的做法。
yield* 实现跨节点控制权的无缝移交
当流程图中 A 算子指向 B 算子时,引擎不会简单地生成 await A(); await B(); 这种线性调用。它会生成一个类似下面的协程链:
async function* executeChain() {
yield* await nodeA(); // 委托执行A,A内部可以多次yield
yield* await nodeB(); // A结束后,B立即接管,并能接收A的最终状态(如return值)
}
这里的关键在于:yield* 不仅转发产出值,还会自动透传 throw() 和 return()。这意味着:
- 如果 B 算子中途抛出错误,该错误可以原路回溯到 A 的上下文中,便于可视化调试器定位断点;
- 如果 A 算子执行完毕后
return { cursor: "abc123" },B 算子通过yield*的委托机制可直接读取该返回值,用于续传分页或幂等标识; - 整个链始终保持单个 generator 实例,内存栈深度恒定,避免了
for await...of嵌套带来的隐式递归膨胀问题。
可视化设计器需暴露“流契约”配置项
仅靠普通拖拽无法发挥 yield* 的优势。必须在设计阶段就引导用户声明流的行为。建议在节点的属性面板上增加三项配置:
- 输入模式:单条事件、批量数组,还是持续流——这决定算子是否启用多次
yield; - 输出契约:是否携带元数据,例如
{ data, meta: { timestamp, seq } },方便下游用yield*解构; - 中断策略:失败时是继续下一条(soft fail)、终止整条链(hard fail),还是交由上游重试(retryable)——这直接影响
yield*如何处理throw。
举例来说,“数据库写入”节点若设为 retryable,其底层实现就会包装一层 yield from retry_wrapper(db_insert()),下游节点通过 yield* 自动继承重试上下文,无需重复配置。这种设计上的巧妙之处,正是让整个链真正“活”起来的关键。
运行时引擎需注入轻量调度上下文
注意,yield* 本身是被动委托,引擎必须主动管理流的节奏。推荐在执行链的顶层注入一个 ContextToken,其中携带:
- 当前的 traceId 与 spanId——用于全链路日志对齐;
- 最大并发度——控制
yield*并行展开的深度; - 超时预算——每个
yield*委托之前都要检查剩余时间。
这样一来,当用户在画布上连接了 12 个算子时,引擎不会生成 12 层嵌套的 yield*,而是动态拆分为若干条子链(例如按事务边界划分)。每条子链内部用 yield* 保证顺序,子链之间用 Promise.allSettled 协调——既保持了语义的清晰,又规避了栈溢出的风险。需要强调的是,整个架构的核心驱动依然是 yield* 这个优雅而强大的委托机制。
