docs: explain asynchronous example ownership and callbacks

This commit is contained in:
2026-11-12 19:34:37 +08:00
parent 8a61d27938
commit a0545e7e40
6 changed files with 58 additions and 0 deletions
+9
View File
@@ -2,17 +2,22 @@
// owner/key 相同的新读取替换旧任务;失败只记录 Task.error,不执行 apply。
// 持有三个显示对象与当前任务;task 初值占位,第一次 read 后替换为真实 Task。
class FileReader {
// 这三个字段保存已构造的文字对象引用,回调更新同一业务对象,不创建一份新的页面或复制 Mount。
output: TextBlock
problem: TextBlock
status: TextBlock
// 初始用通用 Object 占位;read 安装真实 Task 后可查询 status/error,result 需等待成功交付,apply 不会在占位阶段执行。
task: Object
// 仅成功时调用,result 在成功回调期间已可读;写原 TextBlock 触发显示刷新。
function apply(self: FileReader): Unit {
// 任务调度器在 I/O 成功且仍有交付资格时调用绑定的 apply;self 是原 reader,调用者无需另外传参。
self.output.text = self.task.result()
}
// 以 self 为 owner、file 为 key 启动读取,清空旧输出,并把状态/错误文字绑定到当前任务。
function read(self: FileReader, path: String): Unit {
// path 是 Console 本次传入的路径;任务创建同步返回句柄,文件读取在后台,方法这里不会等文件内容。
self.task = ui.readText(self, "file", path, self.apply)
// 清掉上次显示与当前读取状态对齐;已取消的旧任务不能稍后回写覆盖这次读取。
self.output.text = ""
// Task.status/error 是零参数只读方法,可直接作为绑定计算;换任务后绑定也随本次调用更新。
self.status.bind("text", self.task.status)
@@ -21,9 +26,13 @@ class FileReader {
}
// 先建立界面对象,保持 reader 为可从 Console 操作的全局业务入口。
output = TextBlock("尚未读取文件")
// 单独显示 Task.error;失败不会触发 apply,所以读不到新内容时也能通过绑定看到错误。
problem = TextBlock("")
// 独立进度控件绑定到这次任务的方法,普通重绘不会重新提交 readText。
status = TextBlock("")
// 全局 reader 保留 owner 和三个显示对象;ui.state(0) 仅满足 Object 字段初值,不是文件结果。
reader = FileReader(output, problem, status, ui.state(0))
// 页面只是展示输出,任务 owner 是 reader;关闭这个页面不等于直接取消 reader 下的任务。
page = Page()
page.title = "异步文件"
page.render(Column(24,16,TextBlock("在 REPL 调用 reader.read(绝对路径字符串)",2),status,output,problem))
+9
View File
@@ -5,16 +5,21 @@ class TextFetcher {
output: TextBlock
problem: TextBlock
status: TextBlock
// 保存最新请求句柄,owner/key 替换后 apply 只读取当前任务;初始 Object 值尚不是可查询的 Task。
task: Object
// 完整响应留在 VM 业务字段,TextBlock 只显示提示;外部读取和 GUI 排版的内容量分开。
response: String
// 成功后取出完整 UTF-8 结果,写 response 供 Console 查询;provider 失败不会进入此方法。
function apply(self: TextFetcher): Unit {
// 成功交付前调度器已写入 Task 结果;绑定 self 的零参回调在所属 VM 执行,不在网络线程写业务字段。
self.response = self.task.result()
self.output.text = "请求完成;在 REPL 查看 fetcher.response"
}
// 同一 self/request 身份的新请求取消旧交付,重置旧内容并把显示绑定到新任务。
function fetch(self: TextFetcher, url: String): Unit {
// url 来自显式 fetch 调用,HTTP(S) GET 由后台 provider 完成;新句柄返回不代表网络已完成。
self.task = ui.fetchText(self, "request", url, self.apply)
// 同步清旧结果,避免新请求等待期间让 Console 把旧响应误认成新请求结果。
self.response = ""
self.output.text = ""
// 绑定读取当前 Task 方法,完成和失败由 worker 的任务状态变更传播。
@@ -24,9 +29,13 @@ class TextFetcher {
}
// 界面不自动访问网络,用户以 Console 明确指定 URL 后才创建任务。
output = TextBlock("尚未请求网络")
// 错误单独绑定 Task.error,即使失败没有调用 apply 也能展示原因。
problem = TextBlock("")
// 进度显示绑定任务状态,响应内容不拼接进这个控件,避免大正文参与每次布局。
status = TextBlock("")
// 全局业务入口让后续 REPL 复用同一 owner,因此多次 fetch 才会按 request 键替换。
fetcher = TextFetcher(output, problem, status, ui.state(0), "")
// Page 展示与任务身份分开,任务归属 fetcher;保持该业务对象可达时,页面隐藏不自动取消它。
page = Page()
page.title = "异步网络"
page.render(Column(24,16,TextBlock("在 REPL 调用 fetcher.fetch(HTTP 或 HTTPS 地址字符串)",2),status,problem,output))
+9
View File
@@ -4,19 +4,26 @@
class AwaitReader {
output: TextBlock
status: TextBlock
// workflow 句柄由 ui.spawn 返回,用于观察协作函数状态和最终返回值。
task: Object
// provider 句柄由 ui.readText 返回,apply 的 await 等待它;两个字段不是同一个 Task 的别名。
file: Object
response: String
// await 在文件未完成时挂起并保留 self/操作数;恢复后只执行后半段一次,再把字符串作为 workflow.result。
function apply(self: AwaitReader): String {
// 调度器启动 apply 后遇到未完成 file 会保留续体,后续 provider 就绪再恢复;成功结果才赋给 response。
self.response = await self.file
self.output.text = "读取完成;在 REPL 查看 reader.response 或 reader.task.result()"
// 正常返回值成为 workflow.result(),与 file 的读取结果来源相同但属于另一个任务结果。
return self.response
}
// 先创建读任务,再调度普通函数 apply;spawn 而非新 async 语法提供可暂停执行上下文。
function read(self: AwaitReader, path: String): Unit {
// 先安装本轮文件任务,省略成功回调;它以可 await 的结果状态通知 workflow。
self.file = ui.readText(self, "file", path)
// 再安装 workflow,捕获的是绑定 self 的方法;spawn 安排稍后执行,不在这一赋值处同步跑完 apply。
self.task = ui.spawn(self, "workflow", self.apply)
// 启动后清上次业务结果及显示提示,新的 await 恢复后才写入本轮文本。
self.response = ""
self.output.text = "等待文件"
// 显示的是 workflow 状态,等待 provider 时它仍报告 running;结束后 result 可从 Task 或 response 读取。
@@ -24,9 +31,11 @@ class AwaitReader {
}
}
output = TextBlock("在 REPL 调用 reader.read(绝对路径字符串)")
// 只展示协作 workflow 的状态,provider 自身状态可从 reader.file 单独查询。
status = TextBlock("")
// 占位值只用于满足初始字段构造;在 read 内被真实 Task 替换,页面建立不自动读文件。
reader = AwaitReader(output,status,ui.state(0),ui.state(0),"")
// 页面只投影 status/output,两个任务 owner 都是 reader,页面可见与否不是该普通 owner 的交付资格条件。
page = Page()
page.title = "await 文件"
page.render(Column(24,16,status,output))
+10
View File
@@ -2,10 +2,12 @@
// 这里刻意重复增量语句,方便看到暂停点;取消不回滚已经发生的 steps 写入,单个原生调用仍是原子边界。
// 每个实例有独立 steps 状态,两个任务不会把局部帧或计数存储混在一起。
class CooperativeWork {
// 保存 ui.state 返回的原生可变计数,每个 CooperativeWork 构造传入不同对象。
steps: Object
// 普通零参数方法被 spawn 调度后可跨量子继续;每条 increment 更新同一实例的计数。
// 连续语句无需手工 yield,VM 按累计预算暂停,恢复时不会从第一条重放。
function run(self: CooperativeWork): Unit {
// 第 1 至 16 次增量:由 spawn 的调用续体依次执行,每次原生 increment 完成后才进入下一条。
self.steps.increment()
self.steps.increment()
self.steps.increment()
@@ -22,6 +24,7 @@ class CooperativeWork {
self.steps.increment()
self.steps.increment()
self.steps.increment()
// 第 17 至 32 次增量:若此前在 VM 指令量子处让出,恢复后从保存的位置继续,不重跑前面的增量。
self.steps.increment()
self.steps.increment()
self.steps.increment()
@@ -38,6 +41,7 @@ class CooperativeWork {
self.steps.increment()
self.steps.increment()
self.steps.increment()
// 第 33 至 48 次增量:依然修改当前实例的 steps,另一个 owner 的 run 有独立调用帧与计数。
self.steps.increment()
self.steps.increment()
self.steps.increment()
@@ -54,6 +58,7 @@ class CooperativeWork {
self.steps.increment()
self.steps.increment()
self.steps.increment()
// 第 49 至 64 次增量:正常走完才结束任务;中途取消保留已发生修改,不把计数回滚为零。
self.steps.increment()
self.steps.increment()
self.steps.increment()
@@ -74,14 +79,19 @@ class CooperativeWork {
}
// 创建两个独立 owner,虽然 key 都叫 work,也不会相互替换;owner/key 组合才是任务身份。
left = CooperativeWork(ui.state(0))
// 左右实例共享类和方法定义,但这里分别分配 state,不能仅凭相同 work key 判断会相互取消。
right = CooperativeWork(ui.state(0))
// owner 是 left,方法是绑定 left 的 run;Task 句柄只用于状态/取消/结果查询,不是第二个业务计数对象。
leftTask = ui.spawn(left,"work",left.run)
// 同 key 配不同 owner 构成不同任务身份,worker 可以在两个续体之间按量子交替推进。
rightTask = ui.spawn(right,"work",right.run)
// 状态文字绑定到 Task.status,业务进度可在 Console 查询 left/right 的 steps.value。
leftStatus = TextBlock("")
// 两份文字控件分别绑定各自 Task.status,完成时间和状态不会通过同一字段覆盖彼此。
rightStatus = TextBlock("")
leftStatus.bind("text",leftTask.status)
rightStatus.bind("text",rightTask.status)
// 仅展示任务状态,任务 owner 不是 Page;关闭页面不会仅因内容消失就撤掉这两个普通业务 owner 的任务。
page = Page()
page.title = "独立暂停调用"
page.render(Column(24,16,TextBlock("两个任务分别保存执行帧;在 REPL 查看 left.steps.value / right.steps.value",2),leftStatus,rightStatus))
+8
View File
@@ -2,23 +2,31 @@
// 退出先取消任务、关闭窗口,再进行允许的卸载回调;已停止 VM 的清理不能重新进入源代码。
// 在原生文字类型上增加共享日志,用来观察每个 Mount 的卸载通知。
class ExitText: TextBlock {
// 业务对象共享同一日志引用,Mount 参数区分每次出现;不为每个窗口复制一份 ExitText.log。
log: Object
// 每次真实卸载递增同一 log;普通重绘不调用,重复 shutdown 也不应重复调用。
// 宿主挂载生命周期在某出现实际卸载时调用,不由普通 draw/repaint 调用;当前方法不再创建窗口或新任务。
function onUnmount(self: ExitText, mount: Mount): Unit {
// 每次出现通知增加一次共享值,可在保留的另一个视图观察;本例不把 Mount 当作另一份业务状态。
self.log.increment()
}
}
// 全局持有日志,之后 watched 及 textOf 也引用它,关闭一个窗口不意味着日志立即失去业务所有者。
log = ui.state(0)
// 文字业务对象只创建一次,Page 与另一窗都引用它,但两边有各自的 Mount 生命周期。
watched = ExitText("关闭副窗口可观察卸载计数;退出应用也会卸载剩余位置",2,log)
// 主窗口 Page 持有组合根,watched 在组合中形成一次出现;另一页稍后再形成独立出现。
page = Page()
page.title = "退出生命周期"
page.render(Column(24,16,watched,ui.textOf(log,"value")))
// 创建同 VM 内的另一原生窗口;迁移 other 后仍保留 watched 的业务身份。
second = Window()
second.setTitle("Exit lifecycle secondary")
// 独立 Page 使用同一 watched 引用,挂到第二窗口时创建自己的 Mount 和卸载通知路径。
other = Page()
other.render(watched)
// 移动的是 Page 的窗口归属,watched 的共享对象身份保持;不创建第二个 VM 运行同一源码。
other.moveToWindow(second)
// 应用级长定时任务用于观察退出会取消尚未完成的工作,不让退出过程等满 30 秒。
// sleep 不带回调,返回可等待 Task;即使代码保存 pending,全局退出仍先取消任务而不等待计时结束。
pending = ui.sleep(ui,"pending",30000)
+13
View File
@@ -2,31 +2,44 @@
// 每个 owner 下同 key 的重复点击替换旧定时器;任务生命周期不由画面是否可见推断。
// 把 owner 与要修改的 state 分开持有,使同一个调度方法可用于三个作用域。
class Actions {
// 保存任务的生命周期归属,不一定等于回调接收者 self;schedule 把它作为 ui.after 第一参。
owner: Object
// 回调实际修改的共享计数,与 owner 分开,三个 Actions 可以修改同一状态但有不同取消边界。
state: Object
// 定时任务成功交付后修改共享计数,零参数签名符合 timer 回调要求。
// 任务调度器用已绑定 self 的 complete 零参调用,timer 不额外传 Event;自有 state 的增量是交付后的业务修改。
function complete(self: Actions): Unit { self.state.increment() }
// 每次启动 1500ms 的 load 定时器;相同 owner/key 只有最新任务继续等待。
// 三个按钮的 click 事件路由调用 schedule,event 为事件参数;方法内部不读取它,只创建延时任务。
function schedule(self: Actions, event: Event): Unit {
// 返回句柄未存入字段也不立即取消,调度器继续维护任务;同一 owner/load 新任务替换旧等待。
ui.after(self.owner, "load", 1500, self.complete)
}
}
// 唯一业务计数,三种 owner 的成功回调都递增此对象,文字绑定显示同一个 value。
state = ui.state(0)
// 该 Block 从未进入页面树,但全局引用仍持有它,因此它拥有的任务可执行。
hidden = TextBlock("这个 Block 没有挂载,也能拥有任务")
page = Page()
page.title = "任务生命周期"
// 三个 Action 共享一个 state,却分别归属 Page、Block、应用,便于比较关闭页面的取消边界。
// Page 仍在窗口注册表中才具备此 owner 的交付资格,单纯切换标签不等于关闭 Page。
pageActions = Actions(page, state)
// hidden 是普通 Block,未挂载不阻止其拥有任务;全局引用和 Actions 字段保留业务对象。
blockActions = Actions(hidden, state)
// 应用 owner 使用 ui,页面关闭后仍可交付,但整个运行时退出时会统一取消未完成任务。
appActions = Actions(ui, state)
pageButton = Button("Page 任务:关闭 Tab 会取消")
blockButton = Button("隐藏 Block 任务:关闭 Tab 仍继续")
appButton = Button("应用任务:关闭 Tab 仍继续")
// 保存连接对象供显式监听器生命周期管理,按钮的事件路由先调用对应 Actions.schedule 再安装定时器。
pageConnection = pageButton.on("click", pageActions.schedule)
// 监听器绑定 blockActions,实际任务 owner 来自该实例字段 hidden,不误用触发点击的按钮 Mount。
blockConnection = blockButton.on("click", blockActions.schedule)
// 监听器绑定 appActions,所以同一个按钮回调实现可复用而不改变三个不同 owner 身份。
appConnection = appButton.on("click", appActions.schedule)
page.render(Column(24, 16, TextBlock("1.5 秒后更新同一个状态对象;重复点击只保留最新任务"), ui.textOf(state, "value"), pageButton, blockButton, appButton))
// 额外应用级任务延迟 500ms,用于展示没有点击也可按 worker 空闲轮询交付。
// key=initial 与应用按钮的 key=load 不同,不会因启动演示定时器而取消后续 load 任务。
initial = ui.after(ui, "initial", 500, appActions.complete)
ui.window().setTitle("Owned Tasks")