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

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

8.0 KiB
Raw Blame History

插件内部机制

插件系统对使用者只暴露三样东西:声明 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 往上游找。

内部实现:依赖排序

此章节讲解一下插件依赖排序的内部实现。

排序分四步,全部在构造期完成:

  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 导览。