一、Runtime 的角色与位置
低代码引擎的核心分层:
Schema → 描述"要做什么"(结构 / 数据 / 行为)
Runtime → 决定"如何执行"(解析 / 计算 / 调度)
Renderer → 负责"如何呈现"(组件渲染)
Runtime 是中间的执行层------上接 Schema 的静态描述,下接 Renderer 的 UI 渲染。它的职责:
将声明式 Schema 转换为可执行模型,并统一驱动数据、行为与视图更新
Runtime 如何连接 Renderer:IRuntime 接口
Schema ──→ Runtime(解析 + 求值 + 展开)──→ Renderer(模式匹配 + 渲染)
Runtime 和 Renderer 之间的唯一通道是 IRuntime 接口。Renderer 不直接读 Schema,不解析表达式,不处理 condition/loop------它只调 IRuntime 的方法,消费返回结果。
IRuntime 中同一个接口,对 Runtime 来说是"对外 API",对 Renderer 来说是"数据来源"。 这样的设计:同一套 Renderer,注入不同的 Runtime 实例,行为则完全不同:
- 设计态注入
DesignerRuntime:发请求、触发事件等都空实现------画布只关心设计体验(拖拽、选中等),不需要真正的交互逻辑 - 运行态注入
PreviewRuntime:真正干活的 Runtime,实现所有能力
二、IRuntime 接口:7 个方法
IRuntime 是整个 Runtime 的"API 目录", 对外提供这些能力:
typescript
interface IRuntime {
// Schema Runtime ------ condition / loop / props / ctx 一次性解析
resolveSchemaNode(schema, parentCtx): ResolvedSchemaNode
// Expression Engine ------ 解析 props 中的 {{}} 表达式
resolveProps(schema, scopeCtx): Record<string, any>
// RuntimeContext ------ 创建当前节点的运行时上下文
createContext(schema, resolvedProps, parentCtx): RuntimeContext
// 调度系统 ------ 返回根 ctx,供 RendererRoot 订阅更新
getRootContext(): RuntimeContext | undefined
// Lifecycle ------ 执行 onMount / onUnmount
runLifecycle(name, ctx, schema): void
// DataSource ------ 执行 isInit 数据源请求
runDataSource(ctx, schema): void
// Events ------ 将事件声明包装成回调函数
resolveEvents(schema): Record<string, Function>
}
其中 resolveSchemaNode 是 Bridge 的核心方法------一次性完成 condition 判断、loop 展开、props 解析、ctx 创建,Renderer 只需对返回的 kind 做模式匹配:
typescript
// Renderer 侧的全部"逻辑"仅此而已
const result = runtime.resolveSchemaNode(schema, parentCtx)
if (result.kind === 'empty') return null
if (result.kind === 'list') return result.items.map(item => <RendererNodeItem />)
return <RendererNodeItem item={result} />
三、Runtime 核心组成
Runtime 拆解下来是 4 类能力:
| 类别 | 模块 | 解决什么问题 |
|---|---|---|
| 数据驱动 | RuntimeContext + Expression Engine | 数据存储 → 表达式计算 → UI 更新 |
| 结构执行 | Schema Runtime(resolveSchemaNode) | condition / loop / scope 控制流解释 |
| 副作用系统 | Lifecycle + DataSource + Events | 请求 / 生命周期 / 事件 → 改变 state |
| 调度系统 | ctx.set → notifyUpward → rerender | 状态变更触发 UI 更新 |
将它们串起来就是 Runtime 的核心循环:
arduino
Schema
↓ 结构执行 ── resolveSchemaNode(condition / loop / scope)
↓ 数据驱动 ── Expression Engine({{}} 求值)+ RuntimeContext(作用域)
↓
渲染(Renderer)
↓
副作用系统 ── Lifecycle / DataSource / Events → 改变 state
↓
调度系统 ── ctx.set → notifyUpward → rerender → 回到顶部重新解析
3.1 数据驱动:RuntimeContext + Expression Engine
RuntimeContext 是每个节点的运行时作用域,提供 5 个数据槽(state / props /等)和一条作用域链。子节点通过 parent 向上查找变量,和 JavaScript 作用域链机制一致。
Expression Engine 是一个手写的 Tokenizer + 递归下降 Parser + AST Evaluator,负责将 {{state.count + 1}} 求出真实值。详见 低代码 Runtime 策略注入:让表达式引擎真正可扩展
3.2 结构执行:Schema Runtime
resolveSchemaNode 一次性处理 Schema 的控制流语义------condition、loop、作用域传递,返回 ResolvedSchemaNode 联合类型,Renderer 只做模式匹配进行渲染。详见 低代码的逻辑解释能力:Schema Runtime 的设计
3.3 副作用系统:Lifecycle + DataSource + Events
三者共同解决"UI 怎么动起来":Lifecycle 控制执行时机,DataSource 提供声明式数据请求,Events 声明式用户交互能力。
3.4 调度系统:ctx.set → rerender
所有副作用最终通过 ctx.set() 改变状态。向上冒泡 + 整树 rerender。详见 低代码 Runtime 的调度系统:"数据变化 → UI更新"的最小架构
四、完整执行链路
以下面这段 Schema 为例,串联 Runtime 内部各模块如何协作:
json
{
"id": "page",
"state": { "postList": [], "loading": false },
"methods": { "loadList": "async function(ctx) { ... fetch + ctx.set ... }" },
"lifeCycles": { "onMount": "function(ctx) { ctx.methods.loadList(ctx) }" },
"dataSource": { "list": [{ "id": "authorList", "isInit": true, "..." }] },
"children": [
{ "id": "refresh-btn", "events": { "onClick": "loadList" } },
{ "id": "loading-text", "condition": "state.loading" },
{ "id": "post-item", "loop": "state.postList", "props": { "children": "{{post.title}}" } }
]
}
主线流程可以简化为 4 步:
1. 首次渲染
text
Renderer 启动
→ Runtime 解释根 schema
→ 创建根上下文 rootCtx
→ 解析 condition / loop / props
→ 渲染首屏内容
这一阶段的关键点是:首屏只渲染"当前条件下可见"的节点。
如果 condition 不满足,对应节点不会显示;如果 loop 对应的数据还是空数组,对应列表也不会展开。
2. 页面初始化逻辑执行
text
节点挂载完成
→ Runtime 执行 onMount / DataSource
→ 开始加载数据
这里既可以执行命令式逻辑,比如 loadList();也可以触发声明式数据源请求,比如 fetch(...)。
3. 数据变化,触发重新解释
text
ctx.set(...)
→ notifyUpward
→ Renderer 重新渲染
→ Runtime 重新解释 schema
一旦数据变化:
condition会重新判断loop会重新展开- 表达式会重新求值
- UI 会自动更新
例如:
state.loading = true,加载中显示state.postList = [...],文章列表展开data.authorList = [...],作者列表展开
4. 用户交互再次触发同样流程
text
用户点击按钮
→ 调用 methods / 更新 state
→ notifyUpward
→ Runtime 重新解释 schema
→ UI 更新
也就是说,用户交互和初始化加载,本质上走的是同一条链路:改数据 → 触发更新 → Runtime 重新解释 → Renderer 更新界面。
这一段 JSON 代码,体现的就是 Runtime 在协调 7 个子系统的协作:Expression 解析数据绑定、Schema Runtime 处理 condition/loop、Lifecycle 触发初始化、DataSource 请求数据、Events 响应用户交互、RuntimeContext 提供作用域、调度系统驱动 UI 更新。
五、设计边界与演进方向
V1 的关键取舍
V1 优先保证最小闭环可用,做了几个有意的取舍:
- 页面级状态空间 :methods 定义在根节点并在 rootCtx 上执行,模型简单但 loop 内事件无法访问当前 item 上下文。详见 rootCtx 作为执行上下文
- 全树 rerender :任何
ctx.set()都触发整棵树重新解析,但没有依赖追踪。详见 与 Vue 响应式的区别 - Expression 最小能力集 :白名单只有 5 种 AST 节点类型,不支持函数调用和三元表达式。详见 为什么先做最小 AST 能力集
- 两套安全边界 :Expression Engine 用手写 Parser + 白名单机制,JSFunction 用
new Function()保证能力。详见 对比两种方案:new Function VS 解释器
V2 演进方向
| 方向 | 目标 |
|---|---|
| 调度系统 | 依赖追踪、局部更新、scheduler 批处理 |
| 事件系统 | loop 内事件上下文、与 Runtime 集成 |
| Expression Engine | 三元表达式、函数调用(CallExpression)、AST 缓存 |
| DataSource | 传参、插件扩展、与 Runtime 深度结合 |
| Lifecycle | onUpdate等能力增强、async、表达式能力融入 |
| Schema Runtime | 能力增强 slot 等能力 |
六、总结
Runtime 能力一张表概括:
| 问题 | 核心模块 | 实现原理 |
|---|---|---|
| 数据从哪来? | RuntimeContext | 作用域链 + 5 个数据槽,子节点沿 parent 向上查找 |
| 表达式怎么算? | Expression Engine | 手写 Tokenizer + 递归下降 Parser + AST Evaluator |
| condition/loop 怎么处理? | Schema Runtime | resolveSchemaNode 一次性解释,返回 empty / single / list |
| 组件什么时候执行逻辑? | Lifecycle | JSFunction 编译 + 缓存,useEffect 时机触发 |
| 数据请求怎么发? | DataSource | isInit 声明式请求,handler 注入,结果存入 ctx.data |
| 用户交互怎么响应? | Events | 方法名引用 → 回调函数包装,合并到 resolvedProps |
| 状态变了 UI 怎么更新? | 调度系统 | ctx.set → notifyUpward → rootCtx.subscribe → rerender |
| Renderer 怎么消费? | Renderer Bridge | IRuntime 7 个方法,Renderer 只做模式匹配 |
这些模块各司其职,通过 IRuntime 接口串联成完整的执行系统,Renderer 只需消费 IRuntime 返回的结果并完成渲染。最终效果:一段 JSON 驱动一个可交互的页面。