# 异步资源与清理

组织加载、取消、重试、锚点和释放，避免旧结果写入新文档。

## 选择最短的正确生命周期

| 作用域 | 何时结束 | 适合的资源 |
| --- | --- | --- |
| `editor.scope` | 实例销毁 | 实例服务、全局监听 |
| `editor.documentScope` | 替换文档或实例销毁 | 当前文件的保存、上传、查询 |
| ComponentView `mount.scope` | 视图卸载或文档替换 | 图表、预览、尺寸观察 |
| `scope.fork()` | 显式取消或父作用域结束 | 一次输入对应的请求 |

`documentId` 相同不等于同一次文档生命周期。`replaceDocument()` 会结束旧文档作用域，业务请求应该跟随这个信号，而不是只比较文件名。

## 资源租约

~~~ts
const request = editor.documentScope.acquire(
  async signal => {
    const response = await fetch('/api/preview', { signal })
    if (!response.ok) throw new Error(`HTTP ${response.status}`)
    const url = URL.createObjectURL(await response.blob())
    return { value: url, release: () => URL.revokeObjectURL(url) }
  },
  url => { console.log('Ready preview URL', url) },
)
// The host may cancel this request before replacing the document.
request.dispose()
~~~

这是宿主自行实现 `/api/preview` 的接入形状，不是 MSFE 提供的服务。`acquire` 只向仍有效的请求提交结果；取消后才到达的租约也会释放。每次获取都要返回自己拥有的 lease，不能把一个共享对象的 dispose 随意交给多个视图。

`scope.own(cleanup)` 注册幂等清理并返回提前释放函数；`dispose()` 先 abort，再逆序释放。第三方库若不能取消计算，仍要在使用结果前检查信号，并释放迟到资源。

## SourceDefinition.load

字段绑定资源可通过 `defineSource<Attrs, Resource>({ load(attrs, signal) })` 返回 `{ value, release? }`。NodeView 从 `status` 和 `resource` 读取加载状态，不要自己再发一次相同请求。

`attrs` 应保存可持久的业务标识，资源值可以是解析后的 URL、只读展示数据或布局结果。密钥、登录令牌和临时 object URL 不写入 Markdown。图片完整接入见 [图片与外部资源](/customization/images)。

## 错误与重试是 UI 协议

作用域报告错误并不等于自动显示错误界面。组件负责 `loading / ready / error` 状态、可读源码回退和重试动作。每次重试先结束旧子作用域；不能同时保留多张图表或多个网络响应写入口。

分配资源后、返回租约之前抛错也必须清理。可以在创建实例后立即注册清理，或用 try/finally 管理部分初始化。图表例子的 [完整 renderer](/extensions/diagrams) 包含观察器与实例释放。

## 资源完成后写回源码

“请求仍有效”与“原来的源码位置仍有效”是两回事。上传完成后不要用缓存的数字偏移直接插入。创建文档子作用域，并用 `captureAnchor()` 保留原意图；结果到达后重新取得 target，再通过命令写回。详见 [完整异步目标函数](/guide/selection)。

保存不需要用服务端响应替换全文。顺序写入、失败重试和离开前 flush 的完整接入见 [读取与保存](/guide/saving)。

## 清理测试

为加载器提供可手动完成的 Promise：先发 A 再发 B，让 A 最后到达；销毁编辑器后完成请求；使用同一 documentId 换文档；让渲染在初始化中途抛错。验证没有旧 UI、旧源码修改、重复 finalizer 或遗留观察器。
