03 · 数据绑定与作用域
本章讲什么: A2UI 怎么把「数据」喂给「结构」。三件事:用 JSON Pointer 绑值、列表迭代时的相对作用域、输入组件的双向绑定与
formatString表达式。
1. Dynamic* 类型:一个属性的三种身份
A2UI 把「可绑定的属性」统一定义成 Dynamic* 类型(DynamicString/DynamicNumber/DynamicBoolean/DynamicStringList)。一个 Dynamic 属性可以是三者之一(a2ui_protocol.md:128):
| 形态 | 例子 | 含义 |
|---|---|---|
| 字面量 | "text": "Hello" | 写死的值 |
| 路径绑 定 | "text": {"path": "/user/name"} | 指向数据模型某处(JSON Pointer) |
| 函数调用 | "text": {"call": "formatDate", "args": {...}} | 调目录函数,用其返回值 |
这就是为什么上一章 Text 的 text 字段类型是 DynamicStringSchema 而不是 string。
2. 路径解析与作用域
数据绑定用 JSON Pointer(RFC 6901)。同一个路径解析成什么,取决于当前求值作用域(a2ui_protocol.md:724-746)。
2.1 根作用域(默认)
默认所有组件在根作用域。以 / 开头的是绝对路径,永远从数据模型根解析,跟组件嵌在哪无关。例:/user/profile/name。
2.2 集合作用域(列表迭代时的相对路径)
当容器(Column/Row/List)用 ChildList 的模板特性时,会为数组里每个元素创建一个子作用域:
数据: { company: "Acme", employees: [ {name:"Alice"}, {name:"Bob"} ] }
List children = { path: "/employees", componentId: "card_tpl" }
│
├─ 第 0 项 → 子作用域 = /employees/0
│ 模板里的相对路径 "name" → /employees/0/name → "Alice"
│ 模板里的绝对路径 "/company" → "Acme"(仍可访问根)
└─ 第 1 项 → 子作用域 = /employees/1
"name" → /employees/1/name → "Bob"
规则:不以 / 开头的是相对路径,对着当前集合作用域解析;想访问根仍可用绝对路径(a2ui_protocol.md:738-745)。
2.3 实现:DataContext 就是「当前工作目录」
DataContext 是组件看数据的统一入口,它带一个 path——可以理解成数据模型里的「当前工作目录」(data-context.ts:44-62)。相对路径靠它的 resolvePath 拼成绝对路径:
// 真实源码 data-context.ts:347-362(精简)
private resolvePath(path: string): string {
if (path.startsWith('/')) return path; // 绝对路径,原样
if (path === '' || path === '.') return this.path;
let base = this.path; /* 去尾斜杠等 */
return `${base}/${path}`; // 相对路径,拼到当前作用域
}
列表迭代时,GenericBinder 处理 STRUCTURAL 属性会对每个下标调 dataContext.nested(...) 派生一个深一层的 DataContext(generic-binder.ts:264-285、data-context.ts:342-345)——这正是「为每个数组元素建子作用域」的落点。
3. 双向绑定:输入组件如何回写
读/写契约
交互输入组件(TextField/CheckBox/Slider/ChoicePicker/DateTimeInput)与数据模型建立双向绑定(a2ui_protocol.md:800-808):
- 读(Model→View): 渲染时从绑定
path读值;updateDataModel改了值,组件重渲染。 - 写(View→Model): 用户一交互,客户端立刻把新值写回本地数据模型的
path。
响应式
因为本地数据模型是唯一真相源,更新是响应式的:TextField 绑 /user/name,另一个 Text 也绑 /user/name,用户打字时那个 Text 实 时跟着变(a2ui_protocol.md:811-814)。
实现里这个「自动 setter」很巧:GenericBinder 处理 OBJECT 时,为每个 DYNAMIC 属性自动生成一个 setXxx——只有当原属性确实是 {path: ...} 绑定时,setter 才会 dataContext.set(path, newValue) 写回(generic-binder.ts:351-362)。即「写死的字面量不可写回,绑了路径的才可写回」。
关键:本地优先,不自动联网
双向绑定只在客户端本地(a2ui_protocol.md:816-821):打字、勾选不会自动触发网络请求,只更新本地状态。只有一个用户动作(如点提交按钮)被触发时,才把改过的数据随 action 的 context 回传。表单提交模式就是:绑 /formData/email → 用户打字(本地更新)→ 点提交,按钮 action 的 context 引用 {path:"/formData/email"},客户端解析出实际值一并发出。
这把「实时输入体验」和「网络往返」彻底解耦——很重要的一个设计取舍。
4. formatString:字符串里嵌表达式
语法
formatString 让你在字符串里用 ${...} 嵌入动态表达式(a2ui_protocol.md:1050-1095):
- 数据绑定:
${/user/firstName}(绝对)、${firstName}(相对)。 - 函数调用(靠括号识别):
${now()}、${formatDate(value:${/currentDate}, format:'yyyy-MM-dd')}。 - 嵌套:
${upper(${now()})}。 - 转义字面
${:写\${。
实现:一个手写扫描器
ExpressionParser 把含 ${} 的字符串解析成 DynamicValue[]——纯字面段保留为字符串,${...} 段解析成 {path} 或 {call, args}(expression_parser.ts:35-73)。几个值得一看的细节:
// 真实源码 expression_parser.ts:129-162(精简):一个表达式的分支
private parseExpressionInternal(scanner, depth) {
if (scanner.matches('${')) { /* 嵌套块,递归 */ }
if (引号) return this.parseStringLiteral(scanner); // 字面量
if (数字) return this.parseNumberLiteral(scanner);
if (matchesKeyword('true')) return true; /* false / null */
const token = this.scanPathOrIdentifier(scanner); // 标识符
if (scanner.peek() === '(') return this.parseFunctionCall(token, ...); // 有括号→函数
else return {path: token}; // 无括号→路径绑定
}
- 靠括号区分「函数」与「路径」:
foo(...)是函数调用,foo是路径绑定(expression_parser.ts:155-162)。 - 递归深度上限 10(
MAX_DEPTH),防栈溢出(expression_parser.ts:29,36-38)。 - 括号平衡 + 引号穿透:
extractInterpolationContent数{}平衡,且跳过引号内内容,所以函数参数里的字符串不会误伤平衡计数(expression_parser.ts:75-103)。
5. 类型转换
非字符串值插值进字符串时(a2ui_protocol.md:792-797):数字/布尔走标准字符串化;null/undefined → 空串 "";对象/数组 → JSON 字符串化(保证跨客户端一致)。
6. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 数据上下文/作用域 | renderers/web_core/src/v0_9/rendering/data-context.ts | DataContext、resolvePath、nested |
| 反应式求值 | 同上 | resolveSignal、subscribeDynamicValue |
| 自动 setter(回写) | renderers/web_core/src/v0_9/rendering/generic-binder.ts | resolveAndBind(OBJECT 分支) |
| 表达式解析 | renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.ts | ExpressionParser、parseExpressionInternal |
| 规范 | specification/v1_0/docs/a2ui_protocol.md | 「Data model representation」「formatString」节 |