From 459bb189982d7b6598aabb332edf98499e94e2d1 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Tue, 27 Oct 2026 13:59:17 +0800 Subject: [PATCH] =?UTF-8?q?docs(10-38):=20=E6=96=B0=E5=A2=9E=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E5=86=85=E9=83=A8=E6=9C=BA=E5=88=B6=E9=A1=B5=E5=B9=B6?= =?UTF-8?q?=E4=BA=92=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advanced/iii/core-state.md | 2 +- docs/advanced/iii/plugin-internals.md | 121 ++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 docs/advanced/iii/plugin-internals.md diff --git a/docs/advanced/iii/core-state.md b/docs/advanced/iii/core-state.md index 46d4186..9601b78 100644 --- a/docs/advanced/iii/core-state.md +++ b/docs/advanced/iii/core-state.md @@ -178,7 +178,7 @@ export async function createSecondaryState( L0 内部也有先后:道具仓库与怪物仓库依赖图块仓库,所以图块仓库先建。L1 与 L2 里有几个子系统需要回指主对象,构造时就把当前实例传进去:地图、勇士、怪物上下文、战斗、事件系统与寻路都属于这一类。L3 的加载器不反向引用主对象,它只拿加载管理器与 starter,再把核心配置登记为自己的第一份配置。 -四层装完后才处理插件:先遍历插件列表建立注册表,重复 id 在这里告警并跳过;再按每个插件声明的 `dataDeps` 建依赖表,调用一次 `orderPlugins` 得到固定顺序;最后同步执行 `awake`,把已排好序的插件逐个装配。装配期回调结束后,构造器返回,异步初始化与加载还没有开始。 +四层装完后才处理插件:先遍历插件列表建立注册表,重复 id 在这里告警并跳过;再按每个插件声明的 `dataDeps` 建依赖表,调用一次 `orderPlugins` 得到固定顺序;最后同步执行 `awake`,把已排好序的插件逐个装配。装配期回调结束后,构造器返回,异步初始化与加载还没有开始。插件状态推进与依赖排序的细节见[插件内部机制](./plugin-internals.md)。 ## 边界与注意 diff --git a/docs/advanced/iii/plugin-internals.md b/docs/advanced/iii/plugin-internals.md new file mode 100644 index 0000000..a9da805 --- /dev/null +++ b/docs/advanced/iii/plugin-internals.md @@ -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(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('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` 不供读者调用**:它是构造期的内部机制;要控制顺序,声明依赖即可。