# 事件与事务状态

在正确的生命周期订阅变化，区分源码、状态槽和临时存储。

## 先决定状态放在哪里

| 状态 | 归属 | 换文档 / 组件卸载 |
| --- | --- | --- |
| 标题、任务完成值、图表数据 | Markdown 源码和 codec | 随保存持久化 |
| 折叠、映射范围、需要事务计算的值 | `defineState()` | 每份文档独立初始化 |
| 实例服务、缓存入口、清理句柄 | 扩展 `storage` 或 `editor.scope` | 实例销毁时释放 |
| 一个弹窗或请求的结果 | 子 `TaskScope` | 取消、卸载时释放 |
| 一个按钮的 hover 状态 | Vue / React 局部状态 | 组件卸载即丢弃 |

把可持久字段只存到 `storage` 会导致重新打开文件后丢失。反过来，折叠等阅读偏好不应该为了刷新 UI 而修改 Markdown。

## 选择正确的通知

`editor.onChange()` 只观察已接受源码变化，适合保存。扩展 `onTransaction` 包括选区和效果；`onSelectionUpdate` 只在选区变化时执行。通过 `subscribeState()` 观察一个有界派生值，避免每次按键重算整个工具栏。

~~~ts
const initialHead = editor.state.selection.main.head
const unsubscribe = editor.subscribeState({
  select: state => state.selection.main.head,
  change: (head, previous) => console.debug({ head, previous }),
})
editor.scope.own(unsubscribe)
~~~

先自己读取初始值；订阅不承诺补发一次初始通知。默认按 `Object.is` 比较；每次返回新对象时，提供 `equal` 或改为返回稳定标量。`select` 必须无副作用，不应序列化全文或发请求。

## 一个完整的状态槽

下方模块可直接导入；安装 `Hints` 后再读 `hintState`。示例状态仅控制临时阅读偏好，不需要撤销历史。

### recipes/state.ts

```ts
import { Extension, defineEffect, defineState } from "@quirekit/msfe-core";
/** Explicit view-only preference; it is not a persistent Markdown field. */
export const setHints = defineEffect<boolean>();
/** State resets per document and has no undo policy because this preference is source-neutral. */
export const hintState = defineState({ key: "example.hints",
  /** @returns Default visibility. */ create() { return true; },
  /** @param previous - Accepted or draft value. @param update - Pure transaction inputs. @returns The latest explicitly supplied preference. */
  update(previous, update) { return update.effects(setHints).at(-1) ?? previous; },
});
/** Install the slot before reading it or declaring it as a projection dependency. */
export const Hints = Extension.create({ name: "exampleHints",
  /** @returns State owned by this feature installation. */ addState() { return [hintState]; },
});

```

~~~ts
import { Hints, hintState, setHints } from './recipes/state'

// Include Hints in the editor's initial extensions.
const enabled = editor.getState(hintState)
const result = editor.updateViewState({ effects: [setHints.of(!enabled)] })
~~~

`updateViewState()` 不改源码，允许只读模式下的合法视图效果；它拒绝源码修改和重新配置。`update()` 是纯 reducer，可能在命令试算时运行。只读 `update.document` 提供结果文本；依赖解析树的查询放到事务接受后的观察阶段。

默认保存当前滚动位置，适合用户主动展开或收起内容。跟随选区的高亮等被动更新应传入
`preserveScroll: false`，让键盘、查找或大纲尚未完成的滚动继续生效；它本身不滚动，也不改变选区。

```ts
editor.updateViewState({ effects: [setHints.of(enabled)], preserveScroll: false })
```

## 位置、效果和撤销

载荷包含源码偏移时，`defineEffect(map)` 必须提供映射规则。范围集合可使用 `SourceRangeSet` / `mapSourceRange`，明确边界插入和完整删除时如何处理。详见 [选区与异步目标](/guide/selection)。

`defineState({ invert })` 才会为该状态贡献反向效果。返回的反向位置使用事务前坐标；不提供 `invert` 的临时效果不会自动跟随文本撤销。`defineMetadata()` 是事务关联信息，不是可保存字段。

## 控制订阅成本

不要为了按钮启用状态把全文同步到框架响应式对象。订阅选区、只读及所需状态槽；基于 `can()` 和语义查询更新相关按钮。多项布尔状态可用显式相等比较，解析尚未确认时保留原按钮状态或显示不可用状态。

投影读取某状态槽时，在 `states` 中声明依赖，详见 [投影层](/extensions/projections)。精确类型见 [状态 API](/api/state)。
