Files
template/docs/advanced/iii/plugin-internals.md
T
AncTe 93ac3efec8 docs(10-39): 新增高级 III 导览并补齐互链
- 新建 III 导览:读者画像、与 I/II 的区别、前置、八篇地图、阅读路径、边界与自检

- 七篇正文补返回导览,并把已有的兄弟页提及改为真实相对链接(含 event-system 收集章节锚点)
2026-10-27 14:13:10 +08:00

124 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 插件内部机制
插件系统对使用者只暴露三样东西:声明 `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` 不供读者调用**:它是构造期的内部机制;要控制顺序,声明依赖即可。
返回[高级 III 导览](./index.md)。