From a0545e7e4068dbc60d59634e502c8f5652987119 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 19:34:37 +0800 Subject: [PATCH] docs: explain asynchronous example ownership and callbacks --- examples/async-file.endor | 9 +++++++++ examples/async-network.endor | 9 +++++++++ examples/await-file.endor | 9 +++++++++ examples/cooperative-tasks.endor | 10 ++++++++++ examples/exit-lifecycle.endor | 8 ++++++++ examples/owned-tasks.endor | 13 +++++++++++++ 6 files changed, 58 insertions(+) diff --git a/examples/async-file.endor b/examples/async-file.endor index 4ba1cd0..376a8a0 100644 --- a/examples/async-file.endor +++ b/examples/async-file.endor @@ -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)) diff --git a/examples/async-network.endor b/examples/async-network.endor index 64dcda2..2c64b64 100644 --- a/examples/async-network.endor +++ b/examples/async-network.endor @@ -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)) diff --git a/examples/await-file.endor b/examples/await-file.endor index 50858af..88da005 100644 --- a/examples/await-file.endor +++ b/examples/await-file.endor @@ -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)) diff --git a/examples/cooperative-tasks.endor b/examples/cooperative-tasks.endor index 665cab5..30c77b3 100644 --- a/examples/cooperative-tasks.endor +++ b/examples/cooperative-tasks.endor @@ -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)) diff --git a/examples/exit-lifecycle.endor b/examples/exit-lifecycle.endor index a7f153e..56a488b 100644 --- a/examples/exit-lifecycle.endor +++ b/examples/exit-lifecycle.endor @@ -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) diff --git a/examples/owned-tasks.endor b/examples/owned-tasks.endor index 1ec05de..4049a66 100644 --- a/examples/owned-tasks.endor +++ b/examples/owned-tasks.endor @@ -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")