先决定状态放在哪里
| 状态 | 归属 | 换文档 / 组件卸载 |
|---|---|---|
| 标题、任务完成值、图表数据 | Markdown 源码和 codec | 随保存持久化 |
| 折叠、映射范围、需要事务计算的值 | defineState() | 每份文档独立初始化 |
| 实例服务、缓存入口、清理句柄 | 扩展 storage 或 editor.scope | 实例销毁时释放 |
| 一个弹窗或请求的结果 | 子 TaskScope | 取消、卸载时释放 |
| 一个按钮的 hover 状态 | Vue / React 局部状态 | 组件卸载即丢弃 |
把可持久字段只存到 storage 会导致重新打开文件后丢失。反过来,折叠等阅读偏好不应该为了刷新 UI 而修改 Markdown。
选择正确的通知
editor.onChange() 只观察已接受源码变化,适合保存。扩展 onTransaction 包括选区和效果;onSelectionUpdate 只在选区变化时执行。通过 subscribeState() 观察一个有界派生值,避免每次按键重算整个工具栏。
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。示例状态仅控制临时阅读偏好,不需要撤销历史。
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]; },
});
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,让键盘、查找或大纲尚未完成的滚动继续生效;它本身不滚动,也不改变选区。
editor.updateViewState({ effects: [setHints.of(enabled)], preserveScroll: false })
位置、效果和撤销
载荷包含源码偏移时,defineEffect(map) 必须提供映射规则。范围集合可使用 SourceRangeSet / mapSourceRange,明确边界插入和完整删除时如何处理。详见 选区与异步目标。
defineState({ invert }) 才会为该状态贡献反向效果。返回的反向位置使用事务前坐标;不提供 invert 的临时效果不会自动跟随文本撤销。defineMetadata() 是事务关联信息,不是可保存字段。
控制订阅成本
不要为了按钮启用状态把全文同步到框架响应式对象。订阅选区、只读及所需状态槽;基于 can() 和语义查询更新相关按钮。多项布尔状态可用显式相等比较,解析尚未确认时保留原按钮状态或显示不可用状态。