docs(10-38): 新增插件内部机制页并互链

This commit is contained in:
unanmed 2026-10-27 13:59:17 +08:00
parent 44f1cd6526
commit 459bb18998
2 changed files with 122 additions and 1 deletions

View File

@ -178,7 +178,7 @@ export async function createSecondaryState(
L0 内部也有先后:道具仓库与怪物仓库依赖图块仓库,所以图块仓库先建。L1 与 L2 里有几个子系统需要回指主对象,构造时就把当前实例传进去:地图、勇士、怪物上下文、战斗、事件系统与寻路都属于这一类。L3 的加载器不反向引用主对象,它只拿加载管理器与 starter,再把核心配置登记为自己的第一份配置。
四层装完后才处理插件:先遍历插件列表建立注册表,重复 id 在这里告警并跳过;再按每个插件声明的 `dataDeps` 建依赖表,调用一次 `orderPlugins` 得到固定顺序;最后同步执行 `awake`,把已排好序的插件逐个装配。装配期回调结束后,构造器返回,异步初始化与加载还没有开始。
四层装完后才处理插件:先遍历插件列表建立注册表,重复 id 在这里告警并跳过;再按每个插件声明的 `dataDeps` 建依赖表,调用一次 `orderPlugins` 得到固定顺序;最后同步执行 `awake`,把已排好序的插件逐个装配。装配期回调结束后,构造器返回,异步初始化与加载还没有开始。插件状态推进与依赖排序的细节见[插件内部机制](./plugin-internals.md)。
## 边界与注意

View File

@ -0,0 +1,121 @@
# 插件内部机制
插件系统对使用者只暴露三样东西:声明 `dataDeps`、在三个时机里做事、按 id 取回插件。本篇讲这三样背后的机制:插件表怎么变成确定顺序、状态怎么推进、依赖写错会怎样。
怎么写一个插件见[插件系统](../i/plugin-system.md),本篇不重复插件类写法与注册步骤。
## 系统模型
插件机制由五段组成,顺序固定:
```text
插件列表(id → 插件)
└─ 依赖图(每个插件的 dataDeps)
└─ 拓扑序(含环与缺失依赖检测)
└─ 状态机(四个状态)
└─ 三阶段执行(awake → initialize → afterLoad)
```
插件列表在主对象构造时建立。每个插件带一个 `id` 与一份 `dataDeps`;`dataDeps` 是它依赖的其它数据端插件 id。依赖图只由这两样东西构成,不读运行期状态。拓扑序在构造期算一次,之后三个阶段都按这同一个顺序执行。
| 组成 | 作用 |
| ------ | -------------------------------------- |
| 插件表 | 按 id 保存插件与它的当前状态 |
| 依赖图 | 把 `dataDeps` 展开成「谁必须在谁之前」 |
| 拓扑序 | 把依赖图压成一个固定执行顺序 |
| 状态机 | 记录每个插件走到哪一步 |
| 三阶段 | 依次执行装配期、加载前与加载后的回调 |
## 三个阶段怎么执行
三个阶段按同一顺序执行,差别在于时机与是否等待:
| 阶段 | 时机 | 是否等待 | 适合做什么 |
| ------------ | ------------ | ------------ | ------------------------------------ |
| `awake` | 主对象构造期 | 否,同步返回 | 创建插件自己的对象、注册可存档内容 |
| `initialize` | 加载开始前 | 是 | 加载前的异步准备、追加加载配置 |
| `afterLoad` | 加载完成后 | 是 | 读取加载内容、创建依赖内容的初始状态 |
每个阶段收到同一份依赖参数:按 `dataDeps` 顺序排列的插件包装对象,形如 `{ status, plugin }`。参数类型是只读的插件包装数组,读者不应修改它;要调用依赖的能力,取 `.plugin` 并断言成对方的对外接口。
`awake` 的返回值不会被等待,写 `async awake` 也不会阻塞后面的插件;需要等待的事情放到 `initialize`。`initialize` 与 `afterLoad` 都会 `await`,只要有一个不兑现,后面的插件与加载就不会继续。
三个阶段都保证:被依赖的插件先完成同一阶段。插件 B 的 `awake` 开始时,A 的 `awake` 一定已经完成;不构成依赖关系的插件之间,不要假定谁先谁后。
## 取回一个插件
按 id 取回用 `state.getDataPlugin<T>(id)`,返回只读的插件包装对象或 `null`。取回结果不能直接调用,先判空、再检查状态:
```ts
import { ICoreState, IDataPluginDefine, PluginStatus } from '@user/data-state';
import { IMyPlugin } from './types';
// 启动完成后取回插件:判空与状态检查都不能省
export function useMyPlugin(state: ICoreState): void {
const holder = state.getDataPlugin<IMyPlugin>('my-plugin');
if (!holder) {
// 没有注册,或 id 拼错
return;
}
if (holder.status !== PluginStatus.Loaded) {
// 还没走到你要的阶段,此时调用可能读到未装配好的字段
return;
}
holder.plugin.doSomething();
}
```
`getDataPlugin` 只查注册表,不保证插件已经加载完成。启动阶段要用某个插件时,优先在 `dataDeps` 里声明依赖,让顺序由引擎保证;按 id 取回更适合可选的、运行中才发生的调用。
不要在构造完成前取插件;在 `awake` 里取一个没有声明依赖的插件,也可能拿到尚未装配的对象。取回之前先确认加载流程走到哪一步,见 [CoreState 组装与初始化](./core-state.md#加载管线)。
## 失败会怎样
依赖写错或 id 重复时,插件不会报「找不到类」,而是静默不执行,只在控制台留下提示。会看到的现象有这几类:
- **依赖成环**:环上的插件全部被排除,依赖它们的下游也一起被排除;控制台会给出环上的 id。
- **缺失依赖**:`dataDeps` 里写了没有注册的 id,该插件被排除,下游同样被排除;控制台按「插件 id + 缺失的依赖 id」提示。
- **重复 id**:构造期只保留第一个,后一个被跳过;控制台告警一次。
- **状态不符**:某个阶段发现插件不在预期的前置状态,会跳过它并告警;正常流程不该出现,出现通常说明流程被重复驱动。
- **启动后仍未完成**:`load()` 结束时检查所有插件,状态不是 `Loaded` 的逐个告警。
被阻断的插件不会执行任何阶段,因此它的对象、存档注册与加载后初始化都不存在。排查时先看控制台里的插件 id,再顺着 `dataDeps` 往上游找。
## 内部实现:依赖排序
此章节讲解一下插件依赖排序的内部实现。
排序分四步,全部在构造期完成:
1. 查缺失依赖。遍历插件表,若某个 `dataDeps` 里的 id 不在表里,就把该插件与它缺失的 id 记下来。
2. 查环。从每个插件出发,沿依赖边搜索;如果能回到插件自身,就把它标记为环上插件。
3. 传播阻断。先把环上插件与缺失依赖的插件加入阻断集合,再反复扫描:只要某个插件的任一依赖被阻断,它也被阻断。阻断会沿下游扩散。
4. 输出拓扑序。反复扫描未被阻断的插件,每轮输出「所有依赖都已输出」的插件;直到没有新插件可输出为止。
结果分成三份:可执行顺序、环上插件集合、缺失依赖表。构造期只算这一次,之后三个阶段都复用同一份结果。`orderPlugins` 是内部函数,读者不需要自己调用;理解它的目的只是知道顺序为什么固定、依赖写错为什么会连带下游。
## 状态机
每个插件在注册表里带一个状态,四个状态与推进条件如下:
| 状态 | 含义 | 进入条件 |
| ------------- | ------------------- | ----------------- |
| `NotAwakened` | 还没执行装配期回调 | 注册时的初始状态 |
| `Awakened` | 已执行 `awake` | `awake` 正常返回 |
| `Initialized` | 已执行 `initialize` | `initialize` 兑现 |
| `Loaded` | 三阶段都完成 | `afterLoad` 兑现 |
每个阶段只处理处于前一状态的插件:`awake` 只处理 `NotAwakened`,`initialize` 只处理 `Awakened`,`afterLoad` 只处理 `Initialized`。状态不对就跳过并告警。
被阻断的插件不会进入执行顺序,因此永远停在 `NotAwakened`;`load()` 结束时的检查会把它点出来。状态只前进不后退:同一个实例不会重新走一遍阶段,也不支持运行期重置。
## 边界与注意
- **依赖在启动前固定**:构造期把 `dataDeps` 抄进依赖表,之后修改数组不会改变顺序。
- **不支持运行期追加插件**:注册表只在构造期建立,没有追加接口;新增插件要改插件列表并重新创建实例。
- **环或缺失会阻断下游**:不只是出错的插件本身,依赖它的插件也不会执行任何阶段。
- **重复 id 只保留第一个**:构造期告警并跳过后一个,插件列表里不会出现两个同 id 的实例。
- **状态不符会被跳过**:每个阶段只处理处于前一状态的插件,状态不对会告警;不要重复驱动加载流程来「补跑」。
- **数据端依赖与渲染端依赖是两套**:`dataDeps` 只写数据端插件 id,渲染端另有 `clientDeps`;两者不要混用。
- **取回结果要判空并检查状态**:`getDataPlugin` 可能返回 `null`,也可能拿到尚未到达目标状态的插件。
- **`orderPlugins` 不供读者调用**:它是构造期的内部机制;要控制顺序,声明依赖即可。