# 投影层与组件生命周期

正确实现内联装饰、跨行预览、状态依赖和测量。

## 源码、描述和 DOM 三层

源码 codec 读取语义；投影层根据已接受输入返回声明式描述；renderer 才创建 DOM 或框架组件。不能在 `build()` 中创建图表、发网络请求或调用编辑器提交事务。

| 输出 | 典型用途 | 限制 |
| --- | --- | --- |
| `mark` | 高亮、拼写标记 | 装饰范围使用规范源码坐标 |
| `line` | 原生段落/容器行样式 | 锚定行起点 |
| `widget` | 控件、提示 | 组件不可接管正文输入 |
| `replace` + `text` | 源码中立的字面呈现 | 文本不会按 HTML 解释 |
| `replace` + `view` | 图表和复杂预览 | 跨行结构需要 block 层 |

## inline 和 block 不能混用

`placement: 'inline'` 在有界可见范围中工作，不应返回跨行结构性替换。`placement: 'block'` 的结果在 CodeMirror 计算视口之前提供，可参与多行高度计算。

普通节点源码 renderer 的 `placement: 'replace' | 'before'` 是行内视图入口，不能把它等同于任意整块编辑容器。图片可用特定的 `measureHeight` 协议；大型围栏预览使用 block 层，见 [图表完整示例](/extensions/diagrams)。

## 声明依赖和稳定身份

~~~ts
addProjectionLayers() {
  const name = this.name
  return [{
    key: `${name}.highlight`,
    placement: 'inline',
    build(input) {
      return input.query(name).matches.map(match => ({
        type: 'mark', from: match.from, to: match.to, class: 'my-source-mark',
      }))
    },
  }]
}
~~~

层的 `key` 全局唯一；视图条目的 `key` 在层内稳定。不要在 `build()` 中创建新的 renderer 函数，避免每次状态改变都卸载组件。示例保留 renderer 模块级常量，但实例资源仍由组件作用域管理。

读取 `input.selection` 的构建器必须设置 `selection: true`；读取状态槽时把槽列入 `states`。普通源码显露已由运行时处理，不需要因此让每次光标移动都重建全部图表。

`modes` 默认 `live` / `locked`。`suppressDuringComposition` 默认 true：Markdown 编辑中，仅与输入法选区相交的描述暂时回退源码；替换描述以 `revealRange` 判断完整语义范围，行描述以该行判断。其他块保留呈现与组件身份。隐藏标记的文档编辑保持投影，原生正文仍由 CodeMirror 接受组合输入。

组合输入开始/结束只重新物化已有描述，不要求重建整篇层。预览仍在页面上时，“编辑源码”等操作也必须检查 `readSourcePresentation(editor.state).composing`，避免抢走输入法焦点。不要为视觉稳定取消输入保护。冲突替换可能回退源码，可查看 `editor.work.projectionConflicts` 定位所有权争用。

## 普通输入时只更新受影响块

当输出只依赖当前语法块时，可以声明 `incremental: true`。块级层会收到变更后的完整语法块，未受影响的描述映射到新源码位置并保留组件身份；内联层仍按可见范围查询，并复用映射后的组件身份。省略该选项时继续完整构建，以兼容依赖全文的插件。

启用前需满足以下约定：

- 使用 `input.query(name)` 或本次范围内的源码，输出及 `revealRange` 属于所查询的块。不要在每次构建中扫描全文、给全文编号或生成跨块目录。
- 引用链接依赖其他位置的定义时声明 `references: true`，新增、删除或修改定义都会重新构建该层。其他全局依赖仍应使用保守构建。
- `selection` 和 `states` 仍须声明；权限、模式或这些依赖变化时，运行时会重新评估完整范围。
- `key` 在一次构建中标识唯一源码位置，renderer 保持稳定。组件身份由运行时沿源码变化映射，整块覆盖或删除会终止旧身份。

如果组件属性带有源码位置，用 `componentView` 的第三个参数显式映射。下面是放在 `input.query(name).matches.map(...)` 中的视图片段，`renderPreview` 是模块级 renderer：

~~~ts
view: componentView(renderPreview, {
  source: input.document.slice(match.from, match.to),
  range: { from: match.from, to: match.to },
}, (props, offset) => ({
  ...props,
  range: {
    from: props.range.from + offset,
    to: props.range.to + offset,
  },
}))
~~~

映射函数只返回新属性，不创建 DOM、请求资源或提交事务。renderer 的 `update(next)` 必须保存最新属性，点击操作不能闭包捕获第一次挂载的位置；预览内容没有变化时，应保留现有图表实例。这样在前方输入后，即使预览稍后才滚入视口，其“编辑源码”操作也会定位到正确块。

局部更新不等于只解析光标所在行。修改代码围栏、列表/容器边界、表格分隔行或引用定义，可能合法地影响前后块；运行时以解析器确认的结构变化扩大更新范围。可对比 `editor.work` 中的 `syntaxDiffBuilds`、`syntaxDiffChunks`、`syntaxDiffComparisons`、`layerVisitedNodes` 与 `blockBuilds`，并验证 DOM/资源是否复用。当前仍需映射既有描述并生成装饰，不保证所有输入都是恒定成本。

## ComponentView 与 NodeView

| 协议 | 输入 | 场景 |
| --- | --- | --- |
| NodeView | `nodeView.attrs`、`updateAttributes`、`getRange` 等 | 与源码字段绑定的控件 |
| ComponentView | 自定义 `view` 属性 | 搜索、折叠按钮、图表等通用组件 |

Vue 使用 `VueNodeViewRenderer` / `VueComponentViewRenderer`，React 使用对应适配器。两类视图都需要原来的 Provider 树中的 Host；不要给每个节点创建独立应用。

原生 `ComponentViewRenderer<Props>` 取得 `dom`、`props`、`host`、`scope` 和 `requestMeasure`，返回 `update`、`destroy`，可选 `ignoreEvent`。`ignoreEvent` 默认 true；只有明确希望 CodeMirror 消费的事件才返回 false。

## 高度与卸载

挂载容器归运行时所有。renderer 管理自己的子 DOM、监听器、观察器和第三方实例，不改写 `.cm-content`。异步图片或图表尺寸变化后调用 `requestMeasure()`，它合并测量请求。

视图更新应复用资源；源码或关键渲染参数变化时取消旧请求。`destroy()` 移除框架入口，scope 结束时释放资源。二者都要能正确处理未完成的加载。完整模式见 [资源生命周期](/extensions/resources)。

## 需要验证的交互

检查鼠标拖选经过预览、键盘进入源码、搜索命中、只读复制、IME、滚动卸载、快速更新及换文档。先比较工作计数变化和资源存活数量，再检查最终画面；光标移动不应触发全文解析或所有图表重新初始化。协议见 [渲染 API](/api/rendering)。
