docs: explain event binding and object lifecycle examples

This commit is contained in:
2026-11-12 19:43:48 +08:00
parent 9cdbf00961
commit ce17cb6ed3
8 changed files with 50 additions and 0 deletions
+8
View File
@@ -2,6 +2,7 @@
// EventLog 用一个递增序列记录实际次序;改变监听器/传播标记可对比跳过与停止的语义。
// 保存各阶段序号与最近 phase;count 是共享状态,其他字段直接写在本模型中。
class EventLog {
// count 保存跨阶段递增的总序列;四个 Step 保存各阶段最近一次序号,不是各自独立递增的计数器。
count: Object
captureStep: Number
methodStep: Number
@@ -10,18 +11,21 @@ class EventLog {
phase: String
// 祖先捕获阶段先递增序列,再保存 captureStep 和 Event.phase。
function capture(self: EventLog, event: Event): Unit {
// 由 root 的 capture 连接调用,当前 event.phase 来自本次派发路径,不由业务模型自行推断。
self.count.increment()
self.captureStep = self.count.value
self.phase = event.phase
}
// 目标监听器的执行点;它排在目标类型 onEvent 方法之后。
function target(self: EventLog, event: Event): Unit {
// button 的普通连接在目标 onEvent 后调用,读取已被前面阶段推进的同一个 count。
self.count.increment()
self.targetStep = self.count.value
self.phase = event.phase
}
// 祖先冒泡阶段记录最后序号,未被 stopPropagation 时才到达。
function bubble(self: EventLog, event: Event): Unit {
// 由 root 的非捕获连接调用;本例不设置停止标记,因此点击目标后可继续冒泡到这里。
self.count.increment()
self.bubbleStep = self.count.value
self.phase = event.phase
@@ -29,21 +33,25 @@ class EventLog {
}
// 覆盖目标方法,比额外 on(click,...) 监听器更早处理目标阶段事件。
class EventButton: Button {
// 原生 Button 参数之外追加模型引用,onEvent 和三个监听器共同写入该模型。
model: EventLog
// 把模型序列递增后写 methodStep,供断言与 capture/target/bubble 比较。
function onEvent(self: EventButton, event: Event): Unit {
// 分发器查找对象方法后进入这里,无需再给此方法单独注册 on 连接;方法返回后继续目标监听器。
self.model.count.increment()
self.model.methodStep = self.model.count.value
self.model.phase = event.phase
}
}
model = EventLog(ui.state(0), 0, 0, 0, 0, "等待点击")
// 只在顶层创建一次日志,重复点击累计序列,四个 Step 会被各自最近一次阶段调用覆盖。
button = EventButton("捕获 → 对象方法 → 监听器 → 冒泡", model)
root = Column(24, 16, TextBlock("同一对象系统中的事件分派", 2), button, ui.textOf(model.count, "value"), ui.textOf(model, "phase"))
// capture 选项显式选择捕获;不带选项的祖先监听器默认在冒泡阶段执行。
// 返回的令牌可 disconnect,同次派发中断开尚未执行的监听器也应生效。
capture = root.on("click", model.capture, "capture")
target = button.on("click", model.target)
// root 是目标的祖先;若把监听器放在另一无关容器,它不会仅因共享 model 而加入这条传播路径。
bubble = root.on("click", model.bubble)
page = Page()
page.render(root)
+10
View File
@@ -2,47 +2,57 @@
// 关闭临时页仍可因业务引用保留等待任务;releaseCycle 清掉外部引用后,缓存不应让整个引用环常驻。
// 比较操作前后对象数时,关闭页面只解除展示根;holder/pending 外部引用也释放后才应回到基线。
holder = ui.state(0)
// pending.value 后续保存 Task;它和 holder.value 保存的 Page 是两条需要显式清除的外部可达路径。
pending = ui.state(0)
phase = ui.state(0)
// 通过 peer 指回包含自己的 Page,配合等待协程形成包含视觉缓存读依赖的引用环。
class CycleRect: RectBlock {
// 指回 transient 页面;页面又通过 render 的根保留 rect,形成真实托管对象环。
peer: Object
// sleep 归属该 rect,await 保留协程帧;先写 phase=1,只有实际恢复后才写 2 并改 Page 标题。
function wait(self: CycleRect): Unit {
// ui.spawn 调用绑定 wait,sleep 的 owner 仍是同一 rect,键 sleep 与外层 workflow 任务键不同。
gate = ui.sleep(self,"sleep",300000)
phase.set(1)
// 在 gate 完成前挂起此调用,下面两条写入尚未执行;done 只是保存恢复结果,本例不读取它。
done = await gate
phase.set(2)
self.peer.title = "等待已结束"
}
// 故意读取 peer.title,让布局依赖包含循环中的 Page;缓存应借用观察,不增加长期 GC 根。
function measure(self: CycleRect, mount: Mount, constraints: Constraints): MeasureResult {
// source_layout 的只读测量调用;observed 虽不参与数值计算,成员访问仍是需要跟踪的实际读取。
observed = self.peer.title
return MeasureResult(180,40)
}
// 绘制也读取 peer.title,检查绘制缓存与等待帧不会共同意外保活循环。
function paint(self: CycleRect, mount: Mount, geometry: Bounds): DisplayList {
// source_paint 的只读计算也读取环中页面;委托基类生成绘制数据,不把 Page 放进输出命令。
observed = self.peer.title
return RectBlock.paint(self,mount,geometry)
}
}
// 创建临时 Page 和 CycleRect,相互引用后挂载,再把 Page/Task 分别放入两个外部 holder。
function mountCycle(): Unit {
// 供 Console 手工调用的零参入口,加载此文件只定义函数,不立即创建临时环或开始五分钟等待。
transient = Page()
transient.title = "临时引用环"
rect = CycleRect(180,40,8,"#3868D9",transient)
transient.render(rect)
holder.set(transient)
phase.set(0)
// spawn 返回工作流 Task 立即存入 pending,等待期间可经 pending.value.status() 观察,不能在此同步等完成。
pending.set(ui.spawn(rect,"workflow",rect.wait))
}
// 先关闭临时页,随后去掉两个外部引用;等待依赖无需等满五分钟,失去可达性后应整体回收。
function releaseCycle(): Unit {
// 应在 mountCycle 建立 holder 后由 Console 调用;close 先解除展示关系,再清除外部 Page 与 Task 引用。
holder.value.close()
holder.set(0)
pending.set(0)
}
// 基线页面始终存在,用它比较操作前后的托管对象数,不把临时页面当作基线。
base = Page()
// base/holder/pending/phase 这些顶层对象仍保留,比较 GC 数量时不能要求整个会话回到零对象。
base.title = "GC 基线"
base.render(TextBlock("关闭临时 Page 后应回收引用环",2))
+4
View File
@@ -5,13 +5,17 @@ color = "#ED6A5A"
class GlobalPaint: RectBlock {
// 调用原生基类产出新命令,读取当前 color 并写到新命令;不在只读阶段修改全局。
function paint(self: GlobalPaint, mount: Mount, geometry: Bounds): DisplayList {
// source_paint 在当前出现绘制时调用;基类用本轮 geometry 构造新的 DisplayList 和矩形命令。
drawing = RectBlock.paint(self,mount,geometry)
// 右侧按全局名称读取 color,左侧只修改本次新结果;不是写 self.color,也不修改已有业务对象。
drawing.commands.at(0).color = color
// 宿主解码校验返回命令后缓存绘制结果;缓存失效观察上面真实发生的全局 color 读取。
return drawing
}
}
// 显示固定大小矩形和操作提示,后续通过 Console 改同一个会话的 color。
rect = GlobalPaint(480,120,12,"#DDEEFF")
// 构造颜色是原生载荷,但每轮源 paint 会用全局 color 覆盖返回命令,所以初次可见颜色来自 #ED6A5A。
page = Page()
page.title = "全局名字依赖"
page.render(Column(24,16,TextBlock("在 REPL 求值 42 不重绘;修改 color 会更新矩形颜色",2),rect))
+3
View File
@@ -2,13 +2,16 @@
// 两个文字绑定读同一 counter;原生点击和 Console 都能操作该对象并传播显示变化。
// state 提供可写 value 和 increment 方法,textOf 建立显式字段到文本的依赖。
counter = ui.state(9)
// 两次 textOf 分别创建文字展示对象,但读取同一 counter.value;一次 increment 会更新两处绑定。
first = ui.textOf(counter, "value", 2)
second = ui.textOf(counter, "value", 2)
title = TextBlock("Endor:同一份对象,代码与 GUI 共同操作", 2)
button = Button("增加计数")
// 零参数源回调委托原生 state.increment;事件分发器根据签名调用,不强塞 Event 参数。
// 由 Button.onClick 在点击时调用,函数从当前会话全局取得 counter,不维护另一份局部数值。
function increment(): Unit { counter.increment() }
// 把实际函数作为回调而非函数名文本;render 之前只创建业务对象,render 后才有出现位置。
button.onClick(increment)
// Page 和 Column 将已有对象组成展示树;此时不调用 increment,首次绑定读到的值仍为 9。
page = Page()
page.render(Column(24, 16, title, Row(0, 16, first, second, button)))
+7
View File
@@ -2,6 +2,7 @@
// 普通数据变化不会重新挂载;页面关闭时子项先卸载,父项随后卸载,旧 Mount 不因重开而复活。
// order 是统一时序,mounted/unmounted 是子项次数;latest 保存最近通知的实际 Mount。
class LifecycleLog {
// order/mounted/unmounted/latest 为可写 state,四个顺序字段为 Number;构造参数按这里的字段次序传入。
order: Object
mounted: Object
unmounted: Object
@@ -16,14 +17,17 @@ class WatchedText: TextBlock {
log: LifecycleLog
// 记录顺序和次数,保存 Mount 并修改原文字;运行时在回调后重新收敛才发布。
function onMount(self: WatchedText, mount: Mount): Unit {
// 宿主为 child 的每个新出现分别调用;同一 self 会收到两次通知,但传入的是两个不同 Mount。
self.log.order.increment()
self.log.mounted.increment()
self.log.childMounted = self.log.order.value
// 保存最近一次参数供 Console 查询,后一次通知会覆盖前一次;mounted 累计值则保留两次调用。
self.log.latest.set(mount)
self.setText("同一个对象,两个独立挂载位置")
}
// 记录已经脱离的出现位置;保留 Mount 可查询 mounted,但不能再当有效 bounds 使用。
function onUnmount(self: WatchedText, mount: Mount): Unit {
// 展示树移除此出现时由宿主调用,不能通过再次 setText 或保存 mount 让已经卸载的出现重新生效。
self.log.order.increment()
self.log.unmounted.increment()
self.log.childUnmounted = self.log.order.value
@@ -35,11 +39,13 @@ class WatchedColumn: Column {
log: LifecycleLog
// 记录父挂载顺序,不修改容器结构;子项随后各自执行通知。
function onMount(self: WatchedColumn, mount: Mount): Unit {
// 父 Mount 建立后的业务通知,只改日志;初始父通知先于下方两个 child 出现的通知。
self.log.order.increment()
self.log.parentMounted = self.log.order.value
}
// 父卸载时子项卸载已完成,序列号应位于所有子项通知之后。
function onUnmount(self: WatchedColumn, mount: Mount): Unit {
// 对整页的卸载,两个子出现先各计一次,父回调最后把当前总序号写入 parentUnmounted。
self.log.order.increment()
self.log.parentUnmounted = self.log.order.value
}
@@ -47,6 +53,7 @@ class WatchedColumn: Column {
// 多个 state 容器分别保存计数/最后 Mount,root 最后参数是源 log 字段,前面才是可变视觉子项。
log = LifecycleLog(ui.state(0), ui.state(0), ui.state(0), 0, 0, 0, 0, ui.state(0))
child = WatchedText("等待挂载", 2, log)
// 两个 child 参数是同一对象引用;新增 log 字段位于所有原生视觉子项之后,不会被安排成额外孩子。
root = WatchedColumn(24, 16, child, child, TextBlock("挂载次数"), ui.textOf(log.mounted, "value"), TextBlock("卸载次数"), ui.textOf(log.unmounted, "value"), log)
page = Page()
page.render(root)
+6
View File
@@ -10,18 +10,22 @@ class Product {
function shade(self: Product): String { return self.color }
// 点击更新原业务对象,现有绑定会同步文字和矩形,不重新调用整个 present 构造树。
function update(self: Product, event: Event): Unit {
// 由 present 中 button.on 注册的绑定方法调用,self 仍是顶层 product,不是 result 容器或 title。
self.name = "原业务对象已更新"
self.color = "#237A45"
}
// 在指定 Window 上下文创建文字、色块和按钮,连接原 self 的方法后返回组合根。
function present(self: Product, window: Window): Block {
// ui.present 验证显式能力后调用本方法,window 是本次展示上下文;每次调用都会新建下面这些视觉对象。
title = TextBlock(self.name)
swatch = RectBlock(180,48,8,self.color)
// 绑定令牌本身不承载另一份业务状态,绑定计算仍调用同一 Product。
titleBinding = title.bind("text",self.label)
colorBinding = swatch.bind("color",self.shade)
// title/swatch 的初值只是构造输入,后续 getter 绑定读取原 Product,使事件写入能继续传播。
button = Button("调用业务对象方法")
connection = button.on("click",self.update)
// result 保留三个视觉孩子,返回该根给调用方;这里尚未通过 Page.render 建立出现树。
result = Column(24,16,title,swatch,button)
// 保留原始业务身份,便于代码从展示根回到来源;contextWindow 仅记录创建时的窗口编号。
result.source = self
@@ -31,8 +35,10 @@ class Product {
}
// 显式声明能力并验证方法契约;只有同名 present 不会自动成为 Presentable。
ui.implement(Product,Presentable)
// 能力注册针对 Product 类型,实际业务字段在这个唯一实例上初始化;label/shade 将读取此实例。
product = Product("普通对象的显式展示能力", "#526AC7")
// 先获得视觉根,再由 ui.open 把它挂载到页面;最后设置 Page 标题。
view = ui.present(product)
// ui.open 接收上面已经生成的 Block,不会再次把 product 交给 present 重新生成另一棵视觉树。
page = ui.open(view)
page.title = "Presentable"
+6
View File
@@ -2,6 +2,7 @@
// 绑定在目标隐藏时也会更新;两个页面共享同一组 Block,不需要两份模型。
// width/color/label 是唯一业务来源,绑定计算只读,expand 是显式事件写入入口。
class Model {
// 三个业务值由下面三个 getter 分别读取;绑定的目标属性保存在视觉对象上,业务修改入口在 expand。
width: Number
color: String
label: String
@@ -13,21 +14,25 @@ class Model {
function getLabel(self: Model): String { return self.label }
// 一个事件同时改三个属性,随后绑定传播,布局与绘制在完整刷新后发布。
function expand(self: Model, event: Event): Unit {
// 由 clickConnection 派发调用,可写业务状态;三个 getter 则由绑定求值调用,不能反向修改模型。
self.width = 260
self.color = "#237A45"
self.label = "绑定已更新"
}
}
model = Model(120, "#526AC7", "点击更新共享对象")
// 原生构造给初始载荷,后面的 bind 再把这些属性接到模型;label 的初始化字符串也会被绑定结果替换。
rect = RectBlock(120, 40, 8, "#526AC7")
label = TextBlock("初始化")
// 保存三个可断开的绑定令牌;令牌不强持有隐藏目标,目标仍需由业务对象图保留。
widthBinding = rect.bind("width", model.getWidth)
// 每次传入的是已绑定 model 的零参函数,注册不会调用 expand,也不把方法名当作待执行源码字符串。
colorBinding = rect.bind("color", model.getColor)
labelBinding = label.bind("text", model.getLabel)
button = Button("更新宽度、颜色和文字")
clickConnection = button.on("click", model.expand)
root = Column(24, 16, label, rect, button)
// 两个页面共享 root 及其子对象,因此只有一组属性绑定,不需要为每个 Mount 再注册一组。
page = Page()
page.title = "通用属性绑定"
page.render(root)
@@ -35,4 +40,5 @@ page.render(root)
second = Page()
second.title = "同一组 Block"
second.render(root)
// 切回第一页不销毁第二页或重建模型;隐藏页仍引用同一组更新后的业务/视觉对象。
page.activate()
+6
View File
@@ -2,6 +2,7 @@
// onMount 作为业务阶段收集结果;实际布局/绘制计算仍受只读规则约束。
// 把协议探测结果保存到普通业务字段,textOf 将其中一部分显示出来。
class VisualResults {
// width/height 来自测量返回值,childX/childY 来自安排返回值,color 来自新绘制命令;都不是最终 Mount 的几何快照。
width: Number
height: Number
childX: Number
@@ -13,10 +14,13 @@ class ProbeRect: RectBlock {
results: VisualResults
// 有限最大宽 120 约束自然宽 200;另用自定义 Bounds 查询绘制颜色,把返回结果写进共享 results。
function onMount(self: ProbeRect, mount: Mount): Unit {
// 宿主挂载通知进入此业务回调,允许写 results;这里主动调用继承的原生 measure 来查询指定约束下的结果。
measured = self.measure(mount, Constraints(0, 0, 120, 80))
self.results.width = measured.width
self.results.height = measured.height
// 自定义 Bounds 仅作为这次 paint 的输入,不会把当前页面里 rect 的实际布局强制设为该位置和大小。
drawing = self.paint(mount, Bounds(12, 18, 200, 60))
// 第一个命令来自 RectBlock 的原生矩形绘制,本例读取颜色后保存到业务模型供下方 textOf 展示。
self.results.color = drawing.commands.at(0).color
}
}
@@ -25,6 +29,7 @@ class ProbeColumn: Column {
results: VisualResults
// 给定绝对起点(20,30)及容器范围,原生 padding=24 使首子项位于(44,54)。
function onMount(self: ProbeColumn, mount: Mount): Unit {
// 当前父出现的子槽已存在,at(0) 对应下面 root 的标题 TextBlock;查询结果不替换宿主随后发布的最终安排。
positions = self.arrange(mount, Bounds(20, 30, 500, 300))
self.results.childX = positions.children.at(0).x
self.results.childY = positions.children.at(0).y
@@ -32,6 +37,7 @@ class ProbeColumn: Column {
}
// 先创建结果容器,再将它作为源子类字段共享;root 的最后一个参数是该字段,不是视觉子项。
results = VisualResults(0, 0, 0, 0, "等待挂载")
// 父 ProbeColumn 与子 ProbeRect 共享同一结果容器,分别填写自己的探测字段,避免把结果误写到原生宽高载荷。
rect = ProbeRect(200, 60, 12, "#DDEEFF", results)
root = ProbeColumn(24, 16, TextBlock("对象的原生视觉协议", 2), rect, TextBlock("约束测量宽度"), ui.textOf(results, "width"), TextBlock("绘制颜色"), ui.textOf(results, "color"), results)
page = Page()