跳到主要内容

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": {...}}调目录函数,用其返回值

这就是为什么上一章 Texttext 字段类型是 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-285data-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):打字、勾选不会自动触发网络请求,只更新本地状态。只有一个用户动作(如点提交按钮)被触发时,才把改过的数据随 actioncontext 回传。表单提交模式就是:绑 /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.tsDataContextresolvePathnested
反应式求值同上resolveSignalsubscribeDynamicValue
自动 setter(回写)renderers/web_core/src/v0_9/rendering/generic-binder.tsresolveAndBind(OBJECT 分支)
表达式解析renderers/web_core/src/v0_9/basic_catalog/expressions/expression_parser.tsExpressionParserparseExpressionInternal
规范specification/v1_0/docs/a2ui_protocol.md「Data model representation」「formatString」节