低代码 Runtime Lifecycle:逻辑执行时机

一、为什么需要 Runtime Lifecycle

在当前我的低代码 Runtime 中,我已经具备了:

  • 上下文作用域管理 ------ RuntimeContext
  • 表达式执行引擎 ------ Expression Engine
  • Schema 逻辑解释能力 ------ Runtime Schema
  • 基础响应式系统 ------ (ctx.set → UI 更新)

也就是目前能描述了"页面长什么样"、"数据怎么绑定",但还缺少一个关键能力:Runtime 无法解决"逻辑在什么时机被触发"

换句话说:

  • Schema 解决的是「做什么」(声明能力)
  • Runtime Lifecycle 要解决的是「什么时候做」(时序调度)

而这正是低代码走向"动态应用"的关键一步。

一个最真实的场景,如下Schema,表达的是页面加载后从接口拉取数据,并展示出来。

json 复制代码
{
  "state": { "list": [] },
  "methods": {
    "fetchList": {
      "type": "JSFunction",
      "value": "function(ctx) { fetch('/api/list').then(ctx.set('state.list', ...) }"
    }
  },
  "lifeCycles": {
    "onMount": {
      "type": "JSFunction",
      "value": "function(ctx) { ctx.methods.fetchList(ctx) }"
    }
  }
}

那么 fetchList 谁来调用它?什么时候调用?

这就是 Runtime Lifecycle要解决的问题。


二、Lifecycle 在低代码 Runtime 中的角色

从架构角度看,Lifecycle 本质是 Runtime 提供的一种"时序扩展点",允许业务逻辑在特定阶段接入,而不侵入核心渲染流程。

它解决的是"如何在不破坏 Runtime 主流程的情况下,引入副作用逻辑"。

拆开来看,Lifecycle 这套机制主要解决下面几个子问题:

问题 具体含义
何时执行 组件挂载后?卸载前?数据变化时?
执行什么 Schema 里面的 string 函数体,变成可执行的函数,并在合适的时机被调用
执行的上下文 函数体里的 ctx.set('state.list', ...)ctx 是谁

用一句话概括(onMount 钩子为例):在组件挂载后,将 Schema 中的 string 函数体编译成的真实函数,再用当前节点的 RuntimeContext 去调用它


三、生命周期模型设计

3.1 函数存在哪里?------ JSFunction 协议

低代码平台的第一原则:Schema 是应用的完整描述 。数据在 Schema 里、结构在 Schema 里、逻辑也在 Schema 里。

但 JSON 没有函数类型。怎么办?

我参考了常见的 Schema 驱动的平台的做法,用一个描述符来表示函数,即 JSFunction 协议(type + value 描述符),如下:

json 复制代码
{
  "type": "JSFunction",
  "value": "function(ctx) { ctx.set('state.list', [...]) }"
}
  • type: "JSFunction" 是类型标记,告诉 Runtime 这是一段需要编译的函数体
  • value 是函数体的源码字符串

3.2 函数怎么变成可执行代码?------ 编译与执行分离

我的设计是将"编译"和"执行"拆成两个独立阶段:

scss 复制代码
Schema 层(存储)       → methods/lifeCycles 以字符串存储
    ↓
Runtime 编译阶段        → new Function() 把字符串编译成真实函数,缓存起来
    ↓
Runtime 执行阶段        → 在合适时机从缓存取出函数,fn(ctx) 调用

"编译"和"执行"分离是因为编译只需要做一次,但触发可能发生多次。

比如:一个 loop 节点渲染 100 个列表项,只需编译 1 次,可执行 100 次。


3.3 执行时机怎么挂?------ 借力 React useEffect

我目前的低代码引擎的渲染层是基于 React。

React 已经提供了完善的生命周期机制 ------ useEffect,所以直接借力:

typescript 复制代码
useEffect(() => {
    runtime.runLifecycle('onMount', ctx, schema)   // 挂载后执行
    return () => {
        runtime.runLifecycle('onUnmount', ctx, schema) // 卸载前执行
    }
}, [])

Runtime 只负责定义生命周期语义,而具体调度交给渲染层框架(React)


四、实现方案

4.1 整体架构(画架构图)


4.2 编译阶段

编译的核心模块是new Function()

javascript 复制代码
输入字符串: "function(ctx) { ctx.set('state.list', [...]) }"
                    ↓
new Function('"use strict"; return (function(ctx) { ... })')()
                    ↓
输出: 一个真实的 Function 对象

编译的核心阶段:createContext

typescript 复制代码
createContext(schema, resolvedProps, parentCtx?) {
    if (!parentCtx) {
        // 根节点:编译 methods,存入 ctx
        const methods = compileMethods(schema.methods)
        this.rootCtx = createRuntimeContext({ ..., methods })
    }
    // 所有节点:编译 lifeCycles,缓存在 Runtime 内部
    this.compileAndCacheLifeCycles(schema)
}

methods 和 lifeCycles 都在 createContext 这里编译,但存储位置不同:

产物 存储位置 原因
methods ctx.methods 需要被其他代码通过 ctx.methods.fetchList(ctx) 调用
lifeCycles Runtime 内部 Map 只有 Runtime 自己在特定时机调用,不需要暴露

4.3 执行阶段

执行阶段主要是 Renderer 调度,通过 Runtime 提供的接口 runLifecycle 完成,纯粹的查找 + 调用,如下:

typescript 复制代码
runLifecycle(name: 'onMount' | 'onUnmount', ctx, schema) {
    const fn = this.compiledLifeCycles.get(schema.id)?.[name]
    fn(ctx)
}

五、示例说明

一个完整的"页面加载 → 拉取列表 → 渲染"场景:

Schema 表示如下:

json 复制代码
{
  "id": "page",
  "componentName": "div",
  "state": { "list": [], "loading": true },
  "methods": {
    "fetchList": {
      "type": "JSFunction",
      "value": "function(ctx) { setTimeout(function() { ctx.set('state.loading', false); ctx.set('state.list', [{name:'Alice'}, {name:'Bob'}]) }, 1000) }"
    }
  },
  "lifeCycles": {
    "onMount": {
      "type": "JSFunction",
      "value": "function(ctx) { ctx.methods.fetchList(ctx) }"
    }
  },
  "children": [
    { "id": "tip", "componentName": "p", "condition": "state.loading", "props": { "children": "加载中..." } },
    { "id": "item", "componentName": "p", "loop": "state.list", "loopArgs": ["item"], "props": { "children": "{{item.name}}" } }
  ]
}

执行时序:

sequenceDiagram participant App as 页面 participant Root as RendererRoot participant Context as Context编译层 participant Node as RendererNodeItem participant Runtime as Lifecycle调度器 participant Methods as 方法执行层 participant Reactive as 响应式系统 App->>Root: 页面加载 Root->>Context: 编译 Schema(methods / lifecycle) Root->>Node: 渲染节点 Node->>Runtime: 触发 onMount(useEffect) Runtime->>Methods: 调度生命周期函数 Methods->>Methods: fetchList(ctx) Methods->>Methods: 异步请求(setTimeout) Methods->>Reactive: ctx.set(...) Reactive-->>Node: 依赖变更 Node-->>Root: 触发重新渲染

整个过程,所有行为都由 Schema 声明,Runtime 自动编排完成的。


六、关键决策

6.1 为什么用 ctx 参数而不是 this?

javascript 复制代码
// 我选择了这种方式
"function(ctx) { ctx.set('state.list', [...]) }"

// 而不是
"function() { this.set('state.list', [...]) }"

三个原因:

  • 显式优于隐式:ctx 从哪来一目了然,不存在 this 指向歧义
  • 无需绑定 :不需要 .bind() / .call() 等 this 绑定机制
  • 一致性:和 Expression Engine 的求值上下文是同一个 ctx 对象

6.2 两套解析机制共存

引擎中存在两套"把字符串变成可执行逻辑"的机制:

机制 场景 实现 安全性
Expression Engine 数据绑定 {{state.count}} 手写 Parser,白名单 AST 高 --- 只能访问 ctx 上的数据
JSFunction methods / lifeCycles new Function() + "use strict" 中 --- 能执行任意 JS

两者职责不同、互不干涉:

  • Expression Engine 负责读数据(state.count、item.name
  • JSFunction 负责执行逻辑(发请求、改状态、副作用)

6.3 为什么 methods 要通过 createChild 继承?

如果子节点的 ctx 上没有 methods,ctx.methods.fetchList 就是 undefined。解决办法是在 createChild 时显式继承上级的方法:

typescript 复制代码
createChild(extra?) {
    return new RuntimeContext({
        ...extra,
        methods: extra?.methods ?? this.methods,  // 显式继承
    }, this)
}

6.4 为什么 lifeCycles 和 methods 要分别缓存在 Runtime 和 ctx 上?

createContext 编译 methods 和 lifeCycles,分别存在了不同的位置。

methods 存在 ctx 上,是因为其他代码需要通过 ctx.methods.xxx(ctx) 调用它。

而lifeCycles 由 Runtime 调度,在 useEffect 触发时内部调用,所以存在 Runtime 内部的 Map 缓存里,职责更清晰,也避免了 ctx 上的属性膨胀。

七、总结

Runtime Lifecycle 补上了低代码引擎从"静态描述"到"动态应用"的关键一环:

markdown 复制代码
Schema Runtime  → 数据绑定、条件渲染、循环渲染
     +
Lifecycle Runtime(本篇)→ 逻辑执行时机、JSFunction 编译
     =
一段 JSON 描述一个完整的动态页面

核心设计有三件事:

  1. JSFunction 协议 --- 让函数以 JSON 可序列化的形式存在 Schema 中
  2. 编译与执行分离 --- 编译一次,执行多次,缓存避免重复开销
  3. 借力 useEffect --- 不重新发明生命周期,复用 Renderer 层已有的机制

八、展望

当前 V1 已经覆盖了核心的 onMount / onUnmount,后续我打算围绕三个方向继续演进:

1. 生命周期能力增强

  • onUpdate(结合依赖追踪)
  • async lifecycle(异步模型)

2. 表达能力扩展

  • props 支持 JSFunction(事件系统)

3. 调度系统升级

  • 引入 Scheduler 统一生命周期执行,支持批处理、优先级

最终目标是演进为一套"完整的运行时调度系统"。

相关推荐
Jeking2171 天前
低代码平台表单设计器unione form editor组件介绍--多行输入组件
低代码·动态表单·表单设计·表单引擎·unione cloud
Jeking2171 天前
低代码平台表单设计器 unione form editor 布局组件--标签布局
低代码·动态表单·表单设计·表单引擎·unione cloud
一切皆是因缘际会1 天前
AI低代码开发实战:轻量化部署与多场景落地
人工智能·深度学习·低代码·机器学习·ai·架构
踩着两条虫1 天前
可视化设计器组件系统:从交互核心到 AI 智能代理的落地实践
开发语言·前端·人工智能·低代码·设计模式·架构
想你依然心痛2 天前
HarmonyOS 6(API 23)实战:基于悬浮导航、沉浸光感与HMAF的“智流工坊“——低代码可视化智能体编排平台
低代码·华为·harmonyos
低代码布道师3 天前
微搭低代码MBA 培训管理系统实战 41——审批中心
低代码
踩着两条虫3 天前
「AI + 低代码」的可视化设计器
开发语言·前端·低代码·设计模式·架构
2601_957787583 天前
全场景矩阵系统低代码平台:企业级业务快速定制与扩展技术实践
低代码·可视化开发·流程编排
Jeking2173 天前
低代码平台表单设计器 unione form editor 组件介绍--随机输入组件
低代码·动态表单·表单设计·表单引擎·unione cloud