- 新建 III 导览:读者画像、与 I/II 的区别、前置、八篇地图、阅读路径、边界与自检 - 七篇正文补返回导览,并把已有的兄弟页提及改为真实相对链接(含 event-system 收集章节锚点)
8.0 KiB
插件内部机制
插件系统对使用者只暴露三样东西:声明 dataDeps、在三个时机里做事、按 id 取回插件。本篇讲这三样背后的机制:插件表怎么变成确定顺序、状态怎么推进、依赖写错会怎样。
怎么写一个插件见插件系统,本篇不重复插件类写法与注册步骤。
系统模型
插件机制由五段组成,顺序固定:
插件列表(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。取回结果不能直接调用,先判空、再检查状态:
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 组装与初始化。
失败会怎样
依赖写错或 id 重复时,插件不会报「找不到类」,而是静默不执行,只在控制台留下提示。会看到的现象有这几类:
- 依赖成环:环上的插件全部被排除,依赖它们的下游也一起被排除;控制台会给出环上的 id。
- 缺失依赖:
dataDeps里写了没有注册的 id,该插件被排除,下游同样被排除;控制台按「插件 id + 缺失的依赖 id」提示。 - 重复 id:构造期只保留第一个,后一个被跳过;控制台告警一次。
- 状态不符:某个阶段发现插件不在预期的前置状态,会跳过它并告警;正常流程不该出现,出现通常说明流程被重复驱动。
- 启动后仍未完成:
load()结束时检查所有插件,状态不是Loaded的逐个告警。
被阻断的插件不会执行任何阶段,因此它的对象、存档注册与加载后初始化都不存在。排查时先看控制台里的插件 id,再顺着 dataDeps 往上游找。
内部实现:依赖排序
此章节讲解一下插件依赖排序的内部实现。
排序分四步,全部在构造期完成:
- 查缺失依赖。遍历插件表,若某个
dataDeps里的 id 不在表里,就把该插件与它缺失的 id 记下来。 - 查环。从每个插件出发,沿依赖边搜索;如果能回到插件自身,就把它标记为环上插件。
- 传播阻断。先把环上插件与缺失依赖的插件加入阻断集合,再反复扫描:只要某个插件的任一依赖被阻断,它也被阻断。阻断会沿下游扩散。
- 输出拓扑序。反复扫描未被阻断的插件,每轮输出「所有依赖都已输出」的插件;直到没有新插件可输出为止。
结果分成三份:可执行顺序、环上插件集合、缺失依赖表。构造期只算这一次,之后三个阶段都复用同一份结果。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 导览。