@lytjs/common-render-queue 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ // src/index.ts
4
+ var RENDER_PRIORITY_WEIGHT = {
5
+ sync: 0,
6
+ high: 1,
7
+ normal: 2,
8
+ low: 3
9
+ };
10
+ var DEFAULT_OPTIONS = {
11
+ enableMerge: true,
12
+ defaultPriority: "normal"
13
+ };
14
+ var RenderQueue = class {
15
+ /**
16
+ * 创建渲染队列实例。
17
+ * @param host - RendererHost 实例,用于调度时序
18
+ * @param options - 可选的配置项
19
+ */
20
+ constructor(host, options) {
21
+ /** 待执行的渲染操作队列 */
22
+ this.queue = [];
23
+ /** 是否已调度刷新 */
24
+ this.scheduled = false;
25
+ /** 调度的定时器 ID */
26
+ this.timerId = null;
27
+ /** 是否正在执行刷新(防止重入) */
28
+ this.flushing = false;
29
+ /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */
30
+ this.dirty = false;
31
+ this.host = host;
32
+ this.options = { ...DEFAULT_OPTIONS, ...options };
33
+ }
34
+ // ==========================================================
35
+ // 公开方法
36
+ // ==========================================================
37
+ /**
38
+ * 将渲染操作加入队列。
39
+ *
40
+ * 如果启用了操作合并,会尝试合并同一元素的重复操作。
41
+ * 队列不为空时自动调度刷新。
42
+ *
43
+ * @param op - 渲染操作
44
+ */
45
+ enqueue(op) {
46
+ if (this.options.enableMerge) {
47
+ this.tryMerge(op);
48
+ } else {
49
+ this.queue.push(op);
50
+ }
51
+ this.dirty = true;
52
+ this.scheduleFlush();
53
+ }
54
+ /**
55
+ * 同步插队刷新:立即执行队列中所有待处理操作。
56
+ *
57
+ * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。
58
+ */
59
+ flushSync() {
60
+ if (this.flushing) return;
61
+ if (this.timerId !== null) {
62
+ this.host.clearTimeout(this.timerId);
63
+ this.timerId = null;
64
+ }
65
+ this.scheduled = false;
66
+ this.flush();
67
+ }
68
+ /**
69
+ * 清空队列(不执行操作)。
70
+ */
71
+ clear() {
72
+ if (this.timerId !== null) {
73
+ this.host.clearTimeout(this.timerId);
74
+ this.timerId = null;
75
+ }
76
+ this.queue.length = 0;
77
+ this.scheduled = false;
78
+ }
79
+ /**
80
+ * 获取当前队列中的操作数量。
81
+ */
82
+ get size() {
83
+ return this.queue.length;
84
+ }
85
+ /**
86
+ * 销毁队列,清理所有状态。
87
+ */
88
+ dispose() {
89
+ this.clear();
90
+ }
91
+ // ==========================================================
92
+ // 内部方法
93
+ // ==========================================================
94
+ /**
95
+ * 调度异步刷新。
96
+ *
97
+ * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。
98
+ * 如果已经调度过,则跳过。
99
+ */
100
+ scheduleFlush() {
101
+ if (this.scheduled) return;
102
+ this.scheduled = true;
103
+ this.timerId = this.host.setTimeout(() => {
104
+ this.timerId = null;
105
+ this.scheduled = false;
106
+ this.flush();
107
+ }, 0);
108
+ }
109
+ /**
110
+ * 执行队列中所有操作。
111
+ */
112
+ flush() {
113
+ if (this.flushing || this.queue.length === 0) return;
114
+ this.flushing = true;
115
+ if (this.dirty) {
116
+ this.sortQueue();
117
+ this.dirty = false;
118
+ }
119
+ const ops = this.queue;
120
+ this.queue = [];
121
+ for (const op of ops) {
122
+ this.executeOperation(op);
123
+ }
124
+ this.flushing = false;
125
+ if (this.queue.length > 0) {
126
+ this.scheduleFlush();
127
+ }
128
+ }
129
+ /**
130
+ * 执行单个渲染操作。
131
+ * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,
132
+ * 此处仅作为调度容器,实际执行通过 custom 类型传入
133
+ */
134
+ executeOperation(op) {
135
+ switch (op.type) {
136
+ case "custom":
137
+ op.fn();
138
+ break;
139
+ // insert / remove / move / patch 操作由上层 renderer 处理,
140
+ // 此处仅作为调度容器,实际执行通过 custom 类型传入
141
+ // 这些操作在 RenderQueue 中主要用于合并和排序,实际 DOM 操作在 renderer 层执行
142
+ case "insert":
143
+ case "remove":
144
+ case "move":
145
+ case "patch":
146
+ if (__DEV__) {
147
+ console.warn(
148
+ `[lytjs/render-queue] Operation type "${op.type}" should be wrapped in a "custom" operation. Direct ${op.type} operations in RenderQueue are no-ops.`
149
+ );
150
+ }
151
+ break;
152
+ }
153
+ }
154
+ /**
155
+ * 尝试合并重复操作。
156
+ *
157
+ * 合并规则:
158
+ * - 同一元素的 remove 操作只保留最后一个
159
+ * - 同一元素的 custom 操作合并为一次
160
+ * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch
161
+ */
162
+ tryMerge(newOp) {
163
+ const targetKey = this.getOperationKey(newOp);
164
+ if (!targetKey) {
165
+ this.queue.push(newOp);
166
+ return;
167
+ }
168
+ for (let i = this.queue.length - 1; i >= 0; i--) {
169
+ const existing = this.queue[i];
170
+ if (this.getOperationKey(existing) === targetKey) {
171
+ if (newOp.type === "remove" && existing.type === "remove") {
172
+ this.queue[i] = newOp;
173
+ return;
174
+ }
175
+ if (newOp.type === "patch" && existing.type === "patch") {
176
+ this.queue[i] = newOp;
177
+ return;
178
+ }
179
+ }
180
+ }
181
+ this.queue.push(newOp);
182
+ }
183
+ /**
184
+ * 获取操作的标识 key,用于合并判断。
185
+ *
186
+ * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。
187
+ * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫
188
+ * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包
189
+ */
190
+ getOperationKey(op) {
191
+ switch (op.type) {
192
+ case "insert":
193
+ return `insert:${this.getVNodeTypeKey(op.vnode)}`;
194
+ case "remove":
195
+ return `remove:${this.getVNodeTypeKey(op.vnode)}`;
196
+ case "move":
197
+ return `move:${this.getVNodeTypeKey(op.vnode)}`;
198
+ case "patch":
199
+ return `patch:${this.getVNodeTypeKey(op.newVNode)}`;
200
+ case "custom":
201
+ return null;
202
+ }
203
+ }
204
+ /**
205
+ * 获取 VNode 的类型 key,用于操作合并判断。
206
+ * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包
207
+ */
208
+ // FIX: DTS build error - 使用 unknown 中间类型
209
+ getVNodeTypeKey(vnode) {
210
+ const v = vnode;
211
+ const t = v?.type;
212
+ if (typeof t === "string") {
213
+ return t;
214
+ }
215
+ if (v?.key != null) {
216
+ return String(v.key);
217
+ }
218
+ if (typeof t === "function") {
219
+ return t.name ?? String(t);
220
+ }
221
+ if (typeof t === "object" && t != null) {
222
+ const tObj = t;
223
+ return tObj.name != null ? String(tObj.name) : String(t);
224
+ }
225
+ return String(t);
226
+ }
227
+ /**
228
+ * 按优先级排序队列。
229
+ *
230
+ * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。
231
+ * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行
232
+ * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性
233
+ */
234
+ sortQueue() {
235
+ const indexedQueue = this.queue.map((op, index) => ({ op, index }));
236
+ indexedQueue.sort((a, b) => {
237
+ const priorityA = this.getPriority(a.op);
238
+ const priorityB = this.getPriority(b.op);
239
+ const weightA = RENDER_PRIORITY_WEIGHT[priorityA];
240
+ const weightB = RENDER_PRIORITY_WEIGHT[priorityB];
241
+ if (weightA !== weightB) {
242
+ return weightA - weightB;
243
+ }
244
+ return a.index - b.index;
245
+ });
246
+ this.queue = indexedQueue.map((item) => item.op);
247
+ }
248
+ /**
249
+ * 获取操作的优先级。
250
+ */
251
+ getPriority(op) {
252
+ return op.priority ?? this.options.defaultPriority;
253
+ }
254
+ };
255
+
256
+ exports.RENDER_PRIORITY_WEIGHT = RENDER_PRIORITY_WEIGHT;
257
+ exports.RenderQueue = RenderQueue;
258
+ //# sourceMappingURL=index.cjs.map
259
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["// @lytjs/common-render-queue\r\n// 渲染队列:收集同一 tick 内的渲染操作,合并重复操作,支持同步插队刷新\r\n\r\ndeclare const __DEV__: boolean;\r\n\r\nimport type { RendererHost } from '@lytjs/host-contract';\r\n\r\n// ============================================================\r\n// 类型定义\r\n// ============================================================\r\n\r\n/**\r\n * 平台无关的 VNode 接口(最小接口,用于 RenderQueue 内部操作标识)。\r\n */\r\ninterface VNode {\r\n type: string | symbol | object;\r\n props: Record<string, unknown> | null;\r\n children: VNode[] | string | null;\r\n key?: string | number | symbol;\r\n component?: unknown;\r\n el?: unknown;\r\n anchor?: unknown;\r\n}\r\n\r\n/**\r\n * 调度任务优先级(与 AsyncScheduler 保持一致)。\r\n */\r\nexport type RenderPriority = 'sync' | 'high' | 'normal' | 'low';\r\n\r\n/**\r\n * 优先级权重映射(数值越小优先级越高)。\r\n */\r\nexport const RENDER_PRIORITY_WEIGHT: Record<RenderPriority, number> = {\r\n sync: 0,\r\n high: 1,\r\n normal: 2,\r\n low: 3,\r\n};\r\n\r\n/**\r\n * 渲染操作类型。\r\n */\r\nexport type RenderOperation =\r\n | { type: 'insert'; vnode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }\r\n | { type: 'remove'; vnode: VNode; priority?: RenderPriority }\r\n | { type: 'move'; vnode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }\r\n | { type: 'patch'; oldVNode: VNode; newVNode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }\r\n | { type: 'custom'; fn: () => void; priority?: RenderPriority };\r\n\r\n/**\r\n * 渲染队列配置项。\r\n */\r\nexport interface RenderQueueOptions {\r\n /** 是否启用操作合并(默认 true) */\r\n enableMerge?: boolean;\r\n /** 默认优先级(默认 'normal') */\r\n defaultPriority?: RenderPriority;\r\n}\r\n\r\n// ============================================================\r\n// 常量\r\n// ============================================================\r\n\r\n/** 默认队列配置 */\r\nconst DEFAULT_OPTIONS: Required<RenderQueueOptions> = {\r\n enableMerge: true,\r\n defaultPriority: 'normal',\r\n};\r\n\r\n// ============================================================\r\n// RenderQueue\r\n// ============================================================\r\n\r\n/**\r\n * 渲染队列。\r\n *\r\n * 收集同一 tick 内的渲染操作,合并对同一元素的重复操作,\r\n * 通过 host.setTimeout 调度批量执行,支持同步插队刷新(flushSync)。\r\n *\r\n * 支持优先级排序:sync > high > normal > low\r\n *\r\n * @template HN - 宿主节点类型\r\n * @template HE - 宿主元素类型\r\n */\r\nexport class RenderQueue<HN = unknown, HE extends HN = HN> {\r\n /** 待执行的渲染操作队列 */\r\n private queue: RenderOperation[] = [];\r\n\r\n /** 是否已调度刷新 */\r\n private scheduled = false;\r\n\r\n /** 调度的定时器 ID */\r\n private timerId: number | null = null;\r\n\r\n /** 是否正在执行刷新(防止重入) */\r\n private flushing = false;\r\n\r\n /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */\r\n private dirty = false;\r\n\r\n /** RendererHost 实例 */\r\n private host: RendererHost<HN, HE>;\r\n\r\n /** 配置项 */\r\n private options: Required<RenderQueueOptions>;\r\n\r\n /**\r\n * 创建渲染队列实例。\r\n * @param host - RendererHost 实例,用于调度时序\r\n * @param options - 可选的配置项\r\n */\r\n constructor(host: RendererHost<HN, HE>, options?: RenderQueueOptions) {\r\n this.host = host;\r\n this.options = { ...DEFAULT_OPTIONS, ...options };\r\n }\r\n\r\n // ==========================================================\r\n // 公开方法\r\n // ==========================================================\r\n\r\n /**\r\n * 将渲染操作加入队列。\r\n *\r\n * 如果启用了操作合并,会尝试合并同一元素的重复操作。\r\n * 队列不为空时自动调度刷新。\r\n *\r\n * @param op - 渲染操作\r\n */\r\n enqueue(op: RenderOperation): void {\r\n if (this.options.enableMerge) {\r\n this.tryMerge(op);\r\n } else {\r\n this.queue.push(op);\r\n }\r\n // 标记队列需要排序,延迟到 flush 时统一排序\r\n this.dirty = true;\r\n this.scheduleFlush();\r\n }\r\n\r\n /**\r\n * 同步插队刷新:立即执行队列中所有待处理操作。\r\n *\r\n * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。\r\n */\r\n flushSync(): void {\r\n if (this.flushing) return;\r\n\r\n // 取消已调度的异步刷新\r\n if (this.timerId !== null) {\r\n this.host.clearTimeout(this.timerId);\r\n this.timerId = null;\r\n }\r\n this.scheduled = false;\r\n\r\n this.flush();\r\n }\r\n\r\n /**\r\n * 清空队列(不执行操作)。\r\n */\r\n clear(): void {\r\n if (this.timerId !== null) {\r\n this.host.clearTimeout(this.timerId);\r\n this.timerId = null;\r\n }\r\n this.queue.length = 0;\r\n this.scheduled = false;\r\n }\r\n\r\n /**\r\n * 获取当前队列中的操作数量。\r\n */\r\n get size(): number {\r\n return this.queue.length;\r\n }\r\n\r\n /**\r\n * 销毁队列,清理所有状态。\r\n */\r\n dispose(): void {\r\n this.clear();\r\n }\r\n\r\n // ==========================================================\r\n // 内部方法\r\n // ==========================================================\r\n\r\n /**\r\n * 调度异步刷新。\r\n *\r\n * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。\r\n * 如果已经调度过,则跳过。\r\n */\r\n private scheduleFlush(): void {\r\n if (this.scheduled) return;\r\n this.scheduled = true;\r\n this.timerId = this.host.setTimeout(() => {\r\n this.timerId = null;\r\n this.scheduled = false;\r\n this.flush();\r\n }, 0);\r\n }\r\n\r\n /**\r\n * 执行队列中所有操作。\r\n */\r\n private flush(): void {\r\n if (this.flushing || this.queue.length === 0) return;\r\n\r\n this.flushing = true;\r\n\r\n // 如果队列未排序,在 flush 时统一排序一次\r\n if (this.dirty) {\r\n this.sortQueue();\r\n this.dirty = false;\r\n }\r\n\r\n // 取出当前所有操作(防止 flush 过程中新增操作导致无限循环)\r\n const ops = this.queue;\r\n this.queue = [];\r\n\r\n for (const op of ops) {\r\n this.executeOperation(op);\r\n }\r\n\r\n this.flushing = false;\r\n\r\n // 如果 flush 过程中又有新操作入队,继续调度\r\n if (this.queue.length > 0) {\r\n this.scheduleFlush();\r\n }\r\n }\r\n\r\n /**\r\n * 执行单个渲染操作。\r\n * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,\r\n * 此处仅作为调度容器,实际执行通过 custom 类型传入\r\n */\r\n private executeOperation(op: RenderOperation): void {\r\n switch (op.type) {\r\n case 'custom':\r\n op.fn();\r\n break;\r\n // insert / remove / move / patch 操作由上层 renderer 处理,\r\n // 此处仅作为调度容器,实际执行通过 custom 类型传入\r\n // 这些操作在 RenderQueue 中主要用于合并和排序,实际 DOM 操作在 renderer 层执行\r\n case 'insert':\r\n case 'remove':\r\n case 'move':\r\n case 'patch':\r\n // 这些操作类型在 RenderQueue 中仅用于队列管理(合并、排序),\r\n // 实际的 DOM 操作由上层 renderer 通过 custom 类型包装后传入\r\n // 设计意图:RenderQueue 专注于调度,不直接操作 DOM\r\n // FIX: P2-v11-22 在 DEV 模式下添加警告,提醒开发者这些操作类型不应直接执行\r\n if (__DEV__) {\r\n console.warn(\r\n `[lytjs/render-queue] Operation type \"${op.type}\" should be wrapped in a \"custom\" operation. ` +\r\n `Direct ${op.type} operations in RenderQueue are no-ops.`,\r\n );\r\n }\r\n break;\r\n default:\r\n // 未知操作类型,静默忽略(防御性编程)\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 尝试合并重复操作。\r\n *\r\n * 合并规则:\r\n * - 同一元素的 remove 操作只保留最后一个\r\n * - 同一元素的 custom 操作合并为一次\r\n * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch\r\n */\r\n private tryMerge(newOp: RenderOperation): void {\r\n const targetKey = this.getOperationKey(newOp);\r\n if (!targetKey) {\r\n this.queue.push(newOp);\r\n return;\r\n }\r\n\r\n // 从后向前查找可合并的操作\r\n for (let i = this.queue.length - 1; i >= 0; i--) {\r\n const existing = this.queue[i]!;\r\n if (this.getOperationKey(existing) === targetKey) {\r\n // remove 操作:替换为最新的\r\n if (newOp.type === 'remove' && existing.type === 'remove') {\r\n this.queue[i] = newOp;\r\n return;\r\n }\r\n // patch 操作:替换为最新的\r\n if (newOp.type === 'patch' && existing.type === 'patch') {\r\n this.queue[i] = newOp;\r\n return;\r\n }\r\n // custom 操作:合并(保留两者,custom 不做去重)\r\n }\r\n }\r\n\r\n this.queue.push(newOp);\r\n }\r\n\r\n /**\r\n * 获取操作的标识 key,用于合并判断。\r\n *\r\n * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。\r\n * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫\r\n * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包\r\n */\r\n private getOperationKey(op: RenderOperation): string | null {\r\n switch (op.type) {\r\n case 'insert':\r\n return `insert:${this.getVNodeTypeKey(op.vnode)}`;\r\n case 'remove':\r\n return `remove:${this.getVNodeTypeKey(op.vnode)}`;\r\n case 'move':\r\n return `move:${this.getVNodeTypeKey(op.vnode)}`;\r\n case 'patch':\r\n return `patch:${this.getVNodeTypeKey(op.newVNode)}`;\r\n case 'custom':\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * 获取 VNode 的类型 key,用于操作合并判断。\r\n * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包\r\n */\r\n // FIX: DTS build error - 使用 unknown 中间类型\r\n private getVNodeTypeKey(vnode: unknown): string {\r\n const v = vnode as Record<string, unknown> | undefined;\r\n const t = v?.type;\r\n if (typeof t === 'string') {\r\n return t;\r\n }\r\n // 组件类型:优先使用 vnode.key,其次使用 type.name 或 Symbol\r\n if (v?.key != null) {\r\n return String(v.key);\r\n }\r\n if (typeof t === 'function') {\r\n return (t as { name?: string }).name ?? String(t);\r\n }\r\n if (typeof t === 'object' && t != null) {\r\n const tObj = t as Record<string, unknown>;\r\n return tObj.name != null ? String(tObj.name) : String(t);\r\n }\r\n return String(t);\r\n }\r\n\r\n /**\r\n * 按优先级排序队列。\r\n *\r\n * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。\r\n * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行\r\n * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性\r\n */\r\n private sortQueue(): void {\r\n // 为每个操作添加原始索引作为第二排序键\r\n const indexedQueue = this.queue.map((op, index) => ({ op, index }));\r\n indexedQueue.sort((a, b) => {\r\n const priorityA = this.getPriority(a.op);\r\n const priorityB = this.getPriority(b.op);\r\n const weightA = RENDER_PRIORITY_WEIGHT[priorityA];\r\n const weightB = RENDER_PRIORITY_WEIGHT[priorityB];\r\n // 优先级不同时按优先级排序,相同时按原始索引排序(保持插入顺序)\r\n if (weightA !== weightB) {\r\n return weightA - weightB;\r\n }\r\n return a.index - b.index;\r\n });\r\n this.queue = indexedQueue.map((item) => item.op);\r\n }\r\n\r\n /**\r\n * 获取操作的优先级。\r\n */\r\n private getPriority(op: RenderOperation): RenderPriority {\r\n return op.priority ?? this.options.defaultPriority;\r\n }\r\n}\r\n"]}
@@ -0,0 +1,175 @@
1
+ import { RendererHost } from '@lytjs/host-contract';
2
+
3
+ /**
4
+ * 平台无关的 VNode 接口(最小接口,用于 RenderQueue 内部操作标识)。
5
+ */
6
+ interface VNode {
7
+ type: string | symbol | object;
8
+ props: Record<string, unknown> | null;
9
+ children: VNode[] | string | null;
10
+ key?: string | number | symbol;
11
+ component?: unknown;
12
+ el?: unknown;
13
+ anchor?: unknown;
14
+ }
15
+ /**
16
+ * 调度任务优先级(与 AsyncScheduler 保持一致)。
17
+ */
18
+ type RenderPriority = 'sync' | 'high' | 'normal' | 'low';
19
+ /**
20
+ * 优先级权重映射(数值越小优先级越高)。
21
+ */
22
+ declare const RENDER_PRIORITY_WEIGHT: Record<RenderPriority, number>;
23
+ /**
24
+ * 渲染操作类型。
25
+ */
26
+ type RenderOperation = {
27
+ type: 'insert';
28
+ vnode: VNode;
29
+ container: unknown;
30
+ anchor?: unknown;
31
+ priority?: RenderPriority;
32
+ } | {
33
+ type: 'remove';
34
+ vnode: VNode;
35
+ priority?: RenderPriority;
36
+ } | {
37
+ type: 'move';
38
+ vnode: VNode;
39
+ container: unknown;
40
+ anchor?: unknown;
41
+ priority?: RenderPriority;
42
+ } | {
43
+ type: 'patch';
44
+ oldVNode: VNode;
45
+ newVNode: VNode;
46
+ container: unknown;
47
+ anchor?: unknown;
48
+ priority?: RenderPriority;
49
+ } | {
50
+ type: 'custom';
51
+ fn: () => void;
52
+ priority?: RenderPriority;
53
+ };
54
+ /**
55
+ * 渲染队列配置项。
56
+ */
57
+ interface RenderQueueOptions {
58
+ /** 是否启用操作合并(默认 true) */
59
+ enableMerge?: boolean;
60
+ /** 默认优先级(默认 'normal') */
61
+ defaultPriority?: RenderPriority;
62
+ }
63
+ /**
64
+ * 渲染队列。
65
+ *
66
+ * 收集同一 tick 内的渲染操作,合并对同一元素的重复操作,
67
+ * 通过 host.setTimeout 调度批量执行,支持同步插队刷新(flushSync)。
68
+ *
69
+ * 支持优先级排序:sync > high > normal > low
70
+ *
71
+ * @template HN - 宿主节点类型
72
+ * @template HE - 宿主元素类型
73
+ */
74
+ declare class RenderQueue<HN = unknown, HE extends HN = HN> {
75
+ /** 待执行的渲染操作队列 */
76
+ private queue;
77
+ /** 是否已调度刷新 */
78
+ private scheduled;
79
+ /** 调度的定时器 ID */
80
+ private timerId;
81
+ /** 是否正在执行刷新(防止重入) */
82
+ private flushing;
83
+ /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */
84
+ private dirty;
85
+ /** RendererHost 实例 */
86
+ private host;
87
+ /** 配置项 */
88
+ private options;
89
+ /**
90
+ * 创建渲染队列实例。
91
+ * @param host - RendererHost 实例,用于调度时序
92
+ * @param options - 可选的配置项
93
+ */
94
+ constructor(host: RendererHost<HN, HE>, options?: RenderQueueOptions);
95
+ /**
96
+ * 将渲染操作加入队列。
97
+ *
98
+ * 如果启用了操作合并,会尝试合并同一元素的重复操作。
99
+ * 队列不为空时自动调度刷新。
100
+ *
101
+ * @param op - 渲染操作
102
+ */
103
+ enqueue(op: RenderOperation): void;
104
+ /**
105
+ * 同步插队刷新:立即执行队列中所有待处理操作。
106
+ *
107
+ * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。
108
+ */
109
+ flushSync(): void;
110
+ /**
111
+ * 清空队列(不执行操作)。
112
+ */
113
+ clear(): void;
114
+ /**
115
+ * 获取当前队列中的操作数量。
116
+ */
117
+ get size(): number;
118
+ /**
119
+ * 销毁队列,清理所有状态。
120
+ */
121
+ dispose(): void;
122
+ /**
123
+ * 调度异步刷新。
124
+ *
125
+ * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。
126
+ * 如果已经调度过,则跳过。
127
+ */
128
+ private scheduleFlush;
129
+ /**
130
+ * 执行队列中所有操作。
131
+ */
132
+ private flush;
133
+ /**
134
+ * 执行单个渲染操作。
135
+ * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,
136
+ * 此处仅作为调度容器,实际执行通过 custom 类型传入
137
+ */
138
+ private executeOperation;
139
+ /**
140
+ * 尝试合并重复操作。
141
+ *
142
+ * 合并规则:
143
+ * - 同一元素的 remove 操作只保留最后一个
144
+ * - 同一元素的 custom 操作合并为一次
145
+ * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch
146
+ */
147
+ private tryMerge;
148
+ /**
149
+ * 获取操作的标识 key,用于合并判断。
150
+ *
151
+ * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。
152
+ * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫
153
+ * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包
154
+ */
155
+ private getOperationKey;
156
+ /**
157
+ * 获取 VNode 的类型 key,用于操作合并判断。
158
+ * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包
159
+ */
160
+ private getVNodeTypeKey;
161
+ /**
162
+ * 按优先级排序队列。
163
+ *
164
+ * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。
165
+ * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行
166
+ * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性
167
+ */
168
+ private sortQueue;
169
+ /**
170
+ * 获取操作的优先级。
171
+ */
172
+ private getPriority;
173
+ }
174
+
175
+ export { RENDER_PRIORITY_WEIGHT, type RenderOperation, type RenderPriority, RenderQueue, type RenderQueueOptions };
@@ -0,0 +1,175 @@
1
+ import { RendererHost } from '@lytjs/host-contract';
2
+
3
+ /**
4
+ * 平台无关的 VNode 接口(最小接口,用于 RenderQueue 内部操作标识)。
5
+ */
6
+ interface VNode {
7
+ type: string | symbol | object;
8
+ props: Record<string, unknown> | null;
9
+ children: VNode[] | string | null;
10
+ key?: string | number | symbol;
11
+ component?: unknown;
12
+ el?: unknown;
13
+ anchor?: unknown;
14
+ }
15
+ /**
16
+ * 调度任务优先级(与 AsyncScheduler 保持一致)。
17
+ */
18
+ type RenderPriority = 'sync' | 'high' | 'normal' | 'low';
19
+ /**
20
+ * 优先级权重映射(数值越小优先级越高)。
21
+ */
22
+ declare const RENDER_PRIORITY_WEIGHT: Record<RenderPriority, number>;
23
+ /**
24
+ * 渲染操作类型。
25
+ */
26
+ type RenderOperation = {
27
+ type: 'insert';
28
+ vnode: VNode;
29
+ container: unknown;
30
+ anchor?: unknown;
31
+ priority?: RenderPriority;
32
+ } | {
33
+ type: 'remove';
34
+ vnode: VNode;
35
+ priority?: RenderPriority;
36
+ } | {
37
+ type: 'move';
38
+ vnode: VNode;
39
+ container: unknown;
40
+ anchor?: unknown;
41
+ priority?: RenderPriority;
42
+ } | {
43
+ type: 'patch';
44
+ oldVNode: VNode;
45
+ newVNode: VNode;
46
+ container: unknown;
47
+ anchor?: unknown;
48
+ priority?: RenderPriority;
49
+ } | {
50
+ type: 'custom';
51
+ fn: () => void;
52
+ priority?: RenderPriority;
53
+ };
54
+ /**
55
+ * 渲染队列配置项。
56
+ */
57
+ interface RenderQueueOptions {
58
+ /** 是否启用操作合并(默认 true) */
59
+ enableMerge?: boolean;
60
+ /** 默认优先级(默认 'normal') */
61
+ defaultPriority?: RenderPriority;
62
+ }
63
+ /**
64
+ * 渲染队列。
65
+ *
66
+ * 收集同一 tick 内的渲染操作,合并对同一元素的重复操作,
67
+ * 通过 host.setTimeout 调度批量执行,支持同步插队刷新(flushSync)。
68
+ *
69
+ * 支持优先级排序:sync > high > normal > low
70
+ *
71
+ * @template HN - 宿主节点类型
72
+ * @template HE - 宿主元素类型
73
+ */
74
+ declare class RenderQueue<HN = unknown, HE extends HN = HN> {
75
+ /** 待执行的渲染操作队列 */
76
+ private queue;
77
+ /** 是否已调度刷新 */
78
+ private scheduled;
79
+ /** 调度的定时器 ID */
80
+ private timerId;
81
+ /** 是否正在执行刷新(防止重入) */
82
+ private flushing;
83
+ /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */
84
+ private dirty;
85
+ /** RendererHost 实例 */
86
+ private host;
87
+ /** 配置项 */
88
+ private options;
89
+ /**
90
+ * 创建渲染队列实例。
91
+ * @param host - RendererHost 实例,用于调度时序
92
+ * @param options - 可选的配置项
93
+ */
94
+ constructor(host: RendererHost<HN, HE>, options?: RenderQueueOptions);
95
+ /**
96
+ * 将渲染操作加入队列。
97
+ *
98
+ * 如果启用了操作合并,会尝试合并同一元素的重复操作。
99
+ * 队列不为空时自动调度刷新。
100
+ *
101
+ * @param op - 渲染操作
102
+ */
103
+ enqueue(op: RenderOperation): void;
104
+ /**
105
+ * 同步插队刷新:立即执行队列中所有待处理操作。
106
+ *
107
+ * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。
108
+ */
109
+ flushSync(): void;
110
+ /**
111
+ * 清空队列(不执行操作)。
112
+ */
113
+ clear(): void;
114
+ /**
115
+ * 获取当前队列中的操作数量。
116
+ */
117
+ get size(): number;
118
+ /**
119
+ * 销毁队列,清理所有状态。
120
+ */
121
+ dispose(): void;
122
+ /**
123
+ * 调度异步刷新。
124
+ *
125
+ * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。
126
+ * 如果已经调度过,则跳过。
127
+ */
128
+ private scheduleFlush;
129
+ /**
130
+ * 执行队列中所有操作。
131
+ */
132
+ private flush;
133
+ /**
134
+ * 执行单个渲染操作。
135
+ * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,
136
+ * 此处仅作为调度容器,实际执行通过 custom 类型传入
137
+ */
138
+ private executeOperation;
139
+ /**
140
+ * 尝试合并重复操作。
141
+ *
142
+ * 合并规则:
143
+ * - 同一元素的 remove 操作只保留最后一个
144
+ * - 同一元素的 custom 操作合并为一次
145
+ * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch
146
+ */
147
+ private tryMerge;
148
+ /**
149
+ * 获取操作的标识 key,用于合并判断。
150
+ *
151
+ * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。
152
+ * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫
153
+ * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包
154
+ */
155
+ private getOperationKey;
156
+ /**
157
+ * 获取 VNode 的类型 key,用于操作合并判断。
158
+ * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包
159
+ */
160
+ private getVNodeTypeKey;
161
+ /**
162
+ * 按优先级排序队列。
163
+ *
164
+ * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。
165
+ * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行
166
+ * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性
167
+ */
168
+ private sortQueue;
169
+ /**
170
+ * 获取操作的优先级。
171
+ */
172
+ private getPriority;
173
+ }
174
+
175
+ export { RENDER_PRIORITY_WEIGHT, type RenderOperation, type RenderPriority, RenderQueue, type RenderQueueOptions };
package/dist/index.mjs ADDED
@@ -0,0 +1,256 @@
1
+ // src/index.ts
2
+ var RENDER_PRIORITY_WEIGHT = {
3
+ sync: 0,
4
+ high: 1,
5
+ normal: 2,
6
+ low: 3
7
+ };
8
+ var DEFAULT_OPTIONS = {
9
+ enableMerge: true,
10
+ defaultPriority: "normal"
11
+ };
12
+ var RenderQueue = class {
13
+ /**
14
+ * 创建渲染队列实例。
15
+ * @param host - RendererHost 实例,用于调度时序
16
+ * @param options - 可选的配置项
17
+ */
18
+ constructor(host, options) {
19
+ /** 待执行的渲染操作队列 */
20
+ this.queue = [];
21
+ /** 是否已调度刷新 */
22
+ this.scheduled = false;
23
+ /** 调度的定时器 ID */
24
+ this.timerId = null;
25
+ /** 是否正在执行刷新(防止重入) */
26
+ this.flushing = false;
27
+ /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */
28
+ this.dirty = false;
29
+ this.host = host;
30
+ this.options = { ...DEFAULT_OPTIONS, ...options };
31
+ }
32
+ // ==========================================================
33
+ // 公开方法
34
+ // ==========================================================
35
+ /**
36
+ * 将渲染操作加入队列。
37
+ *
38
+ * 如果启用了操作合并,会尝试合并同一元素的重复操作。
39
+ * 队列不为空时自动调度刷新。
40
+ *
41
+ * @param op - 渲染操作
42
+ */
43
+ enqueue(op) {
44
+ if (this.options.enableMerge) {
45
+ this.tryMerge(op);
46
+ } else {
47
+ this.queue.push(op);
48
+ }
49
+ this.dirty = true;
50
+ this.scheduleFlush();
51
+ }
52
+ /**
53
+ * 同步插队刷新:立即执行队列中所有待处理操作。
54
+ *
55
+ * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。
56
+ */
57
+ flushSync() {
58
+ if (this.flushing) return;
59
+ if (this.timerId !== null) {
60
+ this.host.clearTimeout(this.timerId);
61
+ this.timerId = null;
62
+ }
63
+ this.scheduled = false;
64
+ this.flush();
65
+ }
66
+ /**
67
+ * 清空队列(不执行操作)。
68
+ */
69
+ clear() {
70
+ if (this.timerId !== null) {
71
+ this.host.clearTimeout(this.timerId);
72
+ this.timerId = null;
73
+ }
74
+ this.queue.length = 0;
75
+ this.scheduled = false;
76
+ }
77
+ /**
78
+ * 获取当前队列中的操作数量。
79
+ */
80
+ get size() {
81
+ return this.queue.length;
82
+ }
83
+ /**
84
+ * 销毁队列,清理所有状态。
85
+ */
86
+ dispose() {
87
+ this.clear();
88
+ }
89
+ // ==========================================================
90
+ // 内部方法
91
+ // ==========================================================
92
+ /**
93
+ * 调度异步刷新。
94
+ *
95
+ * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。
96
+ * 如果已经调度过,则跳过。
97
+ */
98
+ scheduleFlush() {
99
+ if (this.scheduled) return;
100
+ this.scheduled = true;
101
+ this.timerId = this.host.setTimeout(() => {
102
+ this.timerId = null;
103
+ this.scheduled = false;
104
+ this.flush();
105
+ }, 0);
106
+ }
107
+ /**
108
+ * 执行队列中所有操作。
109
+ */
110
+ flush() {
111
+ if (this.flushing || this.queue.length === 0) return;
112
+ this.flushing = true;
113
+ if (this.dirty) {
114
+ this.sortQueue();
115
+ this.dirty = false;
116
+ }
117
+ const ops = this.queue;
118
+ this.queue = [];
119
+ for (const op of ops) {
120
+ this.executeOperation(op);
121
+ }
122
+ this.flushing = false;
123
+ if (this.queue.length > 0) {
124
+ this.scheduleFlush();
125
+ }
126
+ }
127
+ /**
128
+ * 执行单个渲染操作。
129
+ * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,
130
+ * 此处仅作为调度容器,实际执行通过 custom 类型传入
131
+ */
132
+ executeOperation(op) {
133
+ switch (op.type) {
134
+ case "custom":
135
+ op.fn();
136
+ break;
137
+ // insert / remove / move / patch 操作由上层 renderer 处理,
138
+ // 此处仅作为调度容器,实际执行通过 custom 类型传入
139
+ // 这些操作在 RenderQueue 中主要用于合并和排序,实际 DOM 操作在 renderer 层执行
140
+ case "insert":
141
+ case "remove":
142
+ case "move":
143
+ case "patch":
144
+ if (__DEV__) {
145
+ console.warn(
146
+ `[lytjs/render-queue] Operation type "${op.type}" should be wrapped in a "custom" operation. Direct ${op.type} operations in RenderQueue are no-ops.`
147
+ );
148
+ }
149
+ break;
150
+ }
151
+ }
152
+ /**
153
+ * 尝试合并重复操作。
154
+ *
155
+ * 合并规则:
156
+ * - 同一元素的 remove 操作只保留最后一个
157
+ * - 同一元素的 custom 操作合并为一次
158
+ * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch
159
+ */
160
+ tryMerge(newOp) {
161
+ const targetKey = this.getOperationKey(newOp);
162
+ if (!targetKey) {
163
+ this.queue.push(newOp);
164
+ return;
165
+ }
166
+ for (let i = this.queue.length - 1; i >= 0; i--) {
167
+ const existing = this.queue[i];
168
+ if (this.getOperationKey(existing) === targetKey) {
169
+ if (newOp.type === "remove" && existing.type === "remove") {
170
+ this.queue[i] = newOp;
171
+ return;
172
+ }
173
+ if (newOp.type === "patch" && existing.type === "patch") {
174
+ this.queue[i] = newOp;
175
+ return;
176
+ }
177
+ }
178
+ }
179
+ this.queue.push(newOp);
180
+ }
181
+ /**
182
+ * 获取操作的标识 key,用于合并判断。
183
+ *
184
+ * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。
185
+ * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫
186
+ * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包
187
+ */
188
+ getOperationKey(op) {
189
+ switch (op.type) {
190
+ case "insert":
191
+ return `insert:${this.getVNodeTypeKey(op.vnode)}`;
192
+ case "remove":
193
+ return `remove:${this.getVNodeTypeKey(op.vnode)}`;
194
+ case "move":
195
+ return `move:${this.getVNodeTypeKey(op.vnode)}`;
196
+ case "patch":
197
+ return `patch:${this.getVNodeTypeKey(op.newVNode)}`;
198
+ case "custom":
199
+ return null;
200
+ }
201
+ }
202
+ /**
203
+ * 获取 VNode 的类型 key,用于操作合并判断。
204
+ * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包
205
+ */
206
+ // FIX: DTS build error - 使用 unknown 中间类型
207
+ getVNodeTypeKey(vnode) {
208
+ const v = vnode;
209
+ const t = v?.type;
210
+ if (typeof t === "string") {
211
+ return t;
212
+ }
213
+ if (v?.key != null) {
214
+ return String(v.key);
215
+ }
216
+ if (typeof t === "function") {
217
+ return t.name ?? String(t);
218
+ }
219
+ if (typeof t === "object" && t != null) {
220
+ const tObj = t;
221
+ return tObj.name != null ? String(tObj.name) : String(t);
222
+ }
223
+ return String(t);
224
+ }
225
+ /**
226
+ * 按优先级排序队列。
227
+ *
228
+ * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。
229
+ * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行
230
+ * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性
231
+ */
232
+ sortQueue() {
233
+ const indexedQueue = this.queue.map((op, index) => ({ op, index }));
234
+ indexedQueue.sort((a, b) => {
235
+ const priorityA = this.getPriority(a.op);
236
+ const priorityB = this.getPriority(b.op);
237
+ const weightA = RENDER_PRIORITY_WEIGHT[priorityA];
238
+ const weightB = RENDER_PRIORITY_WEIGHT[priorityB];
239
+ if (weightA !== weightB) {
240
+ return weightA - weightB;
241
+ }
242
+ return a.index - b.index;
243
+ });
244
+ this.queue = indexedQueue.map((item) => item.op);
245
+ }
246
+ /**
247
+ * 获取操作的优先级。
248
+ */
249
+ getPriority(op) {
250
+ return op.priority ?? this.options.defaultPriority;
251
+ }
252
+ };
253
+
254
+ export { RENDER_PRIORITY_WEIGHT, RenderQueue };
255
+ //# sourceMappingURL=index.mjs.map
256
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// @lytjs/common-render-queue\r\n// 渲染队列:收集同一 tick 内的渲染操作,合并重复操作,支持同步插队刷新\r\n\r\ndeclare const __DEV__: boolean;\r\n\r\nimport type { RendererHost } from '@lytjs/host-contract';\r\n\r\n// ============================================================\r\n// 类型定义\r\n// ============================================================\r\n\r\n/**\r\n * 平台无关的 VNode 接口(最小接口,用于 RenderQueue 内部操作标识)。\r\n */\r\ninterface VNode {\r\n type: string | symbol | object;\r\n props: Record<string, unknown> | null;\r\n children: VNode[] | string | null;\r\n key?: string | number | symbol;\r\n component?: unknown;\r\n el?: unknown;\r\n anchor?: unknown;\r\n}\r\n\r\n/**\r\n * 调度任务优先级(与 AsyncScheduler 保持一致)。\r\n */\r\nexport type RenderPriority = 'sync' | 'high' | 'normal' | 'low';\r\n\r\n/**\r\n * 优先级权重映射(数值越小优先级越高)。\r\n */\r\nexport const RENDER_PRIORITY_WEIGHT: Record<RenderPriority, number> = {\r\n sync: 0,\r\n high: 1,\r\n normal: 2,\r\n low: 3,\r\n};\r\n\r\n/**\r\n * 渲染操作类型。\r\n */\r\nexport type RenderOperation =\r\n | { type: 'insert'; vnode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }\r\n | { type: 'remove'; vnode: VNode; priority?: RenderPriority }\r\n | { type: 'move'; vnode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }\r\n | { type: 'patch'; oldVNode: VNode; newVNode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }\r\n | { type: 'custom'; fn: () => void; priority?: RenderPriority };\r\n\r\n/**\r\n * 渲染队列配置项。\r\n */\r\nexport interface RenderQueueOptions {\r\n /** 是否启用操作合并(默认 true) */\r\n enableMerge?: boolean;\r\n /** 默认优先级(默认 'normal') */\r\n defaultPriority?: RenderPriority;\r\n}\r\n\r\n// ============================================================\r\n// 常量\r\n// ============================================================\r\n\r\n/** 默认队列配置 */\r\nconst DEFAULT_OPTIONS: Required<RenderQueueOptions> = {\r\n enableMerge: true,\r\n defaultPriority: 'normal',\r\n};\r\n\r\n// ============================================================\r\n// RenderQueue\r\n// ============================================================\r\n\r\n/**\r\n * 渲染队列。\r\n *\r\n * 收集同一 tick 内的渲染操作,合并对同一元素的重复操作,\r\n * 通过 host.setTimeout 调度批量执行,支持同步插队刷新(flushSync)。\r\n *\r\n * 支持优先级排序:sync > high > normal > low\r\n *\r\n * @template HN - 宿主节点类型\r\n * @template HE - 宿主元素类型\r\n */\r\nexport class RenderQueue<HN = unknown, HE extends HN = HN> {\r\n /** 待执行的渲染操作队列 */\r\n private queue: RenderOperation[] = [];\r\n\r\n /** 是否已调度刷新 */\r\n private scheduled = false;\r\n\r\n /** 调度的定时器 ID */\r\n private timerId: number | null = null;\r\n\r\n /** 是否正在执行刷新(防止重入) */\r\n private flushing = false;\r\n\r\n /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */\r\n private dirty = false;\r\n\r\n /** RendererHost 实例 */\r\n private host: RendererHost<HN, HE>;\r\n\r\n /** 配置项 */\r\n private options: Required<RenderQueueOptions>;\r\n\r\n /**\r\n * 创建渲染队列实例。\r\n * @param host - RendererHost 实例,用于调度时序\r\n * @param options - 可选的配置项\r\n */\r\n constructor(host: RendererHost<HN, HE>, options?: RenderQueueOptions) {\r\n this.host = host;\r\n this.options = { ...DEFAULT_OPTIONS, ...options };\r\n }\r\n\r\n // ==========================================================\r\n // 公开方法\r\n // ==========================================================\r\n\r\n /**\r\n * 将渲染操作加入队列。\r\n *\r\n * 如果启用了操作合并,会尝试合并同一元素的重复操作。\r\n * 队列不为空时自动调度刷新。\r\n *\r\n * @param op - 渲染操作\r\n */\r\n enqueue(op: RenderOperation): void {\r\n if (this.options.enableMerge) {\r\n this.tryMerge(op);\r\n } else {\r\n this.queue.push(op);\r\n }\r\n // 标记队列需要排序,延迟到 flush 时统一排序\r\n this.dirty = true;\r\n this.scheduleFlush();\r\n }\r\n\r\n /**\r\n * 同步插队刷新:立即执行队列中所有待处理操作。\r\n *\r\n * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。\r\n */\r\n flushSync(): void {\r\n if (this.flushing) return;\r\n\r\n // 取消已调度的异步刷新\r\n if (this.timerId !== null) {\r\n this.host.clearTimeout(this.timerId);\r\n this.timerId = null;\r\n }\r\n this.scheduled = false;\r\n\r\n this.flush();\r\n }\r\n\r\n /**\r\n * 清空队列(不执行操作)。\r\n */\r\n clear(): void {\r\n if (this.timerId !== null) {\r\n this.host.clearTimeout(this.timerId);\r\n this.timerId = null;\r\n }\r\n this.queue.length = 0;\r\n this.scheduled = false;\r\n }\r\n\r\n /**\r\n * 获取当前队列中的操作数量。\r\n */\r\n get size(): number {\r\n return this.queue.length;\r\n }\r\n\r\n /**\r\n * 销毁队列,清理所有状态。\r\n */\r\n dispose(): void {\r\n this.clear();\r\n }\r\n\r\n // ==========================================================\r\n // 内部方法\r\n // ==========================================================\r\n\r\n /**\r\n * 调度异步刷新。\r\n *\r\n * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。\r\n * 如果已经调度过,则跳过。\r\n */\r\n private scheduleFlush(): void {\r\n if (this.scheduled) return;\r\n this.scheduled = true;\r\n this.timerId = this.host.setTimeout(() => {\r\n this.timerId = null;\r\n this.scheduled = false;\r\n this.flush();\r\n }, 0);\r\n }\r\n\r\n /**\r\n * 执行队列中所有操作。\r\n */\r\n private flush(): void {\r\n if (this.flushing || this.queue.length === 0) return;\r\n\r\n this.flushing = true;\r\n\r\n // 如果队列未排序,在 flush 时统一排序一次\r\n if (this.dirty) {\r\n this.sortQueue();\r\n this.dirty = false;\r\n }\r\n\r\n // 取出当前所有操作(防止 flush 过程中新增操作导致无限循环)\r\n const ops = this.queue;\r\n this.queue = [];\r\n\r\n for (const op of ops) {\r\n this.executeOperation(op);\r\n }\r\n\r\n this.flushing = false;\r\n\r\n // 如果 flush 过程中又有新操作入队,继续调度\r\n if (this.queue.length > 0) {\r\n this.scheduleFlush();\r\n }\r\n }\r\n\r\n /**\r\n * 执行单个渲染操作。\r\n * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,\r\n * 此处仅作为调度容器,实际执行通过 custom 类型传入\r\n */\r\n private executeOperation(op: RenderOperation): void {\r\n switch (op.type) {\r\n case 'custom':\r\n op.fn();\r\n break;\r\n // insert / remove / move / patch 操作由上层 renderer 处理,\r\n // 此处仅作为调度容器,实际执行通过 custom 类型传入\r\n // 这些操作在 RenderQueue 中主要用于合并和排序,实际 DOM 操作在 renderer 层执行\r\n case 'insert':\r\n case 'remove':\r\n case 'move':\r\n case 'patch':\r\n // 这些操作类型在 RenderQueue 中仅用于队列管理(合并、排序),\r\n // 实际的 DOM 操作由上层 renderer 通过 custom 类型包装后传入\r\n // 设计意图:RenderQueue 专注于调度,不直接操作 DOM\r\n // FIX: P2-v11-22 在 DEV 模式下添加警告,提醒开发者这些操作类型不应直接执行\r\n if (__DEV__) {\r\n console.warn(\r\n `[lytjs/render-queue] Operation type \"${op.type}\" should be wrapped in a \"custom\" operation. ` +\r\n `Direct ${op.type} operations in RenderQueue are no-ops.`,\r\n );\r\n }\r\n break;\r\n default:\r\n // 未知操作类型,静默忽略(防御性编程)\r\n break;\r\n }\r\n }\r\n\r\n /**\r\n * 尝试合并重复操作。\r\n *\r\n * 合并规则:\r\n * - 同一元素的 remove 操作只保留最后一个\r\n * - 同一元素的 custom 操作合并为一次\r\n * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch\r\n */\r\n private tryMerge(newOp: RenderOperation): void {\r\n const targetKey = this.getOperationKey(newOp);\r\n if (!targetKey) {\r\n this.queue.push(newOp);\r\n return;\r\n }\r\n\r\n // 从后向前查找可合并的操作\r\n for (let i = this.queue.length - 1; i >= 0; i--) {\r\n const existing = this.queue[i]!;\r\n if (this.getOperationKey(existing) === targetKey) {\r\n // remove 操作:替换为最新的\r\n if (newOp.type === 'remove' && existing.type === 'remove') {\r\n this.queue[i] = newOp;\r\n return;\r\n }\r\n // patch 操作:替换为最新的\r\n if (newOp.type === 'patch' && existing.type === 'patch') {\r\n this.queue[i] = newOp;\r\n return;\r\n }\r\n // custom 操作:合并(保留两者,custom 不做去重)\r\n }\r\n }\r\n\r\n this.queue.push(newOp);\r\n }\r\n\r\n /**\r\n * 获取操作的标识 key,用于合并判断。\r\n *\r\n * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。\r\n * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫\r\n * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包\r\n */\r\n private getOperationKey(op: RenderOperation): string | null {\r\n switch (op.type) {\r\n case 'insert':\r\n return `insert:${this.getVNodeTypeKey(op.vnode)}`;\r\n case 'remove':\r\n return `remove:${this.getVNodeTypeKey(op.vnode)}`;\r\n case 'move':\r\n return `move:${this.getVNodeTypeKey(op.vnode)}`;\r\n case 'patch':\r\n return `patch:${this.getVNodeTypeKey(op.newVNode)}`;\r\n case 'custom':\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * 获取 VNode 的类型 key,用于操作合并判断。\r\n * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包\r\n */\r\n // FIX: DTS build error - 使用 unknown 中间类型\r\n private getVNodeTypeKey(vnode: unknown): string {\r\n const v = vnode as Record<string, unknown> | undefined;\r\n const t = v?.type;\r\n if (typeof t === 'string') {\r\n return t;\r\n }\r\n // 组件类型:优先使用 vnode.key,其次使用 type.name 或 Symbol\r\n if (v?.key != null) {\r\n return String(v.key);\r\n }\r\n if (typeof t === 'function') {\r\n return (t as { name?: string }).name ?? String(t);\r\n }\r\n if (typeof t === 'object' && t != null) {\r\n const tObj = t as Record<string, unknown>;\r\n return tObj.name != null ? String(tObj.name) : String(t);\r\n }\r\n return String(t);\r\n }\r\n\r\n /**\r\n * 按优先级排序队列。\r\n *\r\n * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。\r\n * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行\r\n * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性\r\n */\r\n private sortQueue(): void {\r\n // 为每个操作添加原始索引作为第二排序键\r\n const indexedQueue = this.queue.map((op, index) => ({ op, index }));\r\n indexedQueue.sort((a, b) => {\r\n const priorityA = this.getPriority(a.op);\r\n const priorityB = this.getPriority(b.op);\r\n const weightA = RENDER_PRIORITY_WEIGHT[priorityA];\r\n const weightB = RENDER_PRIORITY_WEIGHT[priorityB];\r\n // 优先级不同时按优先级排序,相同时按原始索引排序(保持插入顺序)\r\n if (weightA !== weightB) {\r\n return weightA - weightB;\r\n }\r\n return a.index - b.index;\r\n });\r\n this.queue = indexedQueue.map((item) => item.op);\r\n }\r\n\r\n /**\r\n * 获取操作的优先级。\r\n */\r\n private getPriority(op: RenderOperation): RenderPriority {\r\n return op.priority ?? this.options.defaultPriority;\r\n }\r\n}\r\n"]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@lytjs/common-render-queue",
3
+ "version": "6.0.0",
4
+ "description": "Render queue for batching and merging render operations in LytJS",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "src"
18
+ ],
19
+ "scripts": {
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "test:coverage": "vitest run --coverage",
23
+ "build": "tsup",
24
+ "type-check": "tsc --noEmit",
25
+ "clean": "rm -rf dist"
26
+ },
27
+ "sideEffects": false,
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@lytjs/host-contract": "^6.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "tsup": "^8.4.0",
34
+ "typescript": "^5.8.2",
35
+ "vitest": "^3.0.7"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,381 @@
1
+ // @lytjs/common-render-queue
2
+ // 渲染队列:收集同一 tick 内的渲染操作,合并重复操作,支持同步插队刷新
3
+
4
+ declare const __DEV__: boolean;
5
+
6
+ import type { RendererHost } from '@lytjs/host-contract';
7
+
8
+ // ============================================================
9
+ // 类型定义
10
+ // ============================================================
11
+
12
+ /**
13
+ * 平台无关的 VNode 接口(最小接口,用于 RenderQueue 内部操作标识)。
14
+ */
15
+ interface VNode {
16
+ type: string | symbol | object;
17
+ props: Record<string, unknown> | null;
18
+ children: VNode[] | string | null;
19
+ key?: string | number | symbol;
20
+ component?: unknown;
21
+ el?: unknown;
22
+ anchor?: unknown;
23
+ }
24
+
25
+ /**
26
+ * 调度任务优先级(与 AsyncScheduler 保持一致)。
27
+ */
28
+ export type RenderPriority = 'sync' | 'high' | 'normal' | 'low';
29
+
30
+ /**
31
+ * 优先级权重映射(数值越小优先级越高)。
32
+ */
33
+ export const RENDER_PRIORITY_WEIGHT: Record<RenderPriority, number> = {
34
+ sync: 0,
35
+ high: 1,
36
+ normal: 2,
37
+ low: 3,
38
+ };
39
+
40
+ /**
41
+ * 渲染操作类型。
42
+ */
43
+ export type RenderOperation =
44
+ | { type: 'insert'; vnode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }
45
+ | { type: 'remove'; vnode: VNode; priority?: RenderPriority }
46
+ | { type: 'move'; vnode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }
47
+ | { type: 'patch'; oldVNode: VNode; newVNode: VNode; container: unknown; anchor?: unknown; priority?: RenderPriority }
48
+ | { type: 'custom'; fn: () => void; priority?: RenderPriority };
49
+
50
+ /**
51
+ * 渲染队列配置项。
52
+ */
53
+ export interface RenderQueueOptions {
54
+ /** 是否启用操作合并(默认 true) */
55
+ enableMerge?: boolean;
56
+ /** 默认优先级(默认 'normal') */
57
+ defaultPriority?: RenderPriority;
58
+ }
59
+
60
+ // ============================================================
61
+ // 常量
62
+ // ============================================================
63
+
64
+ /** 默认队列配置 */
65
+ const DEFAULT_OPTIONS: Required<RenderQueueOptions> = {
66
+ enableMerge: true,
67
+ defaultPriority: 'normal',
68
+ };
69
+
70
+ // ============================================================
71
+ // RenderQueue
72
+ // ============================================================
73
+
74
+ /**
75
+ * 渲染队列。
76
+ *
77
+ * 收集同一 tick 内的渲染操作,合并对同一元素的重复操作,
78
+ * 通过 host.setTimeout 调度批量执行,支持同步插队刷新(flushSync)。
79
+ *
80
+ * 支持优先级排序:sync > high > normal > low
81
+ *
82
+ * @template HN - 宿主节点类型
83
+ * @template HE - 宿主元素类型
84
+ */
85
+ export class RenderQueue<HN = unknown, HE extends HN = HN> {
86
+ /** 待执行的渲染操作队列 */
87
+ private queue: RenderOperation[] = [];
88
+
89
+ /** 是否已调度刷新 */
90
+ private scheduled = false;
91
+
92
+ /** 调度的定时器 ID */
93
+ private timerId: number | null = null;
94
+
95
+ /** 是否正在执行刷新(防止重入) */
96
+ private flushing = false;
97
+
98
+ /** 队列是否需要排序(dirty 标志,延迟到 flush 时排序) */
99
+ private dirty = false;
100
+
101
+ /** RendererHost 实例 */
102
+ private host: RendererHost<HN, HE>;
103
+
104
+ /** 配置项 */
105
+ private options: Required<RenderQueueOptions>;
106
+
107
+ /**
108
+ * 创建渲染队列实例。
109
+ * @param host - RendererHost 实例,用于调度时序
110
+ * @param options - 可选的配置项
111
+ */
112
+ constructor(host: RendererHost<HN, HE>, options?: RenderQueueOptions) {
113
+ this.host = host;
114
+ this.options = { ...DEFAULT_OPTIONS, ...options };
115
+ }
116
+
117
+ // ==========================================================
118
+ // 公开方法
119
+ // ==========================================================
120
+
121
+ /**
122
+ * 将渲染操作加入队列。
123
+ *
124
+ * 如果启用了操作合并,会尝试合并同一元素的重复操作。
125
+ * 队列不为空时自动调度刷新。
126
+ *
127
+ * @param op - 渲染操作
128
+ */
129
+ enqueue(op: RenderOperation): void {
130
+ if (this.options.enableMerge) {
131
+ this.tryMerge(op);
132
+ } else {
133
+ this.queue.push(op);
134
+ }
135
+ // 标记队列需要排序,延迟到 flush 时统一排序
136
+ this.dirty = true;
137
+ this.scheduleFlush();
138
+ }
139
+
140
+ /**
141
+ * 同步插队刷新:立即执行队列中所有待处理操作。
142
+ *
143
+ * 用于需要立即更新 DOM 的场景(如用户交互后读取布局信息)。
144
+ */
145
+ flushSync(): void {
146
+ if (this.flushing) return;
147
+
148
+ // 取消已调度的异步刷新
149
+ if (this.timerId !== null) {
150
+ this.host.clearTimeout(this.timerId);
151
+ this.timerId = null;
152
+ }
153
+ this.scheduled = false;
154
+
155
+ this.flush();
156
+ }
157
+
158
+ /**
159
+ * 清空队列(不执行操作)。
160
+ */
161
+ clear(): void {
162
+ if (this.timerId !== null) {
163
+ this.host.clearTimeout(this.timerId);
164
+ this.timerId = null;
165
+ }
166
+ this.queue.length = 0;
167
+ this.scheduled = false;
168
+ }
169
+
170
+ /**
171
+ * 获取当前队列中的操作数量。
172
+ */
173
+ get size(): number {
174
+ return this.queue.length;
175
+ }
176
+
177
+ /**
178
+ * 销毁队列,清理所有状态。
179
+ */
180
+ dispose(): void {
181
+ this.clear();
182
+ }
183
+
184
+ // ==========================================================
185
+ // 内部方法
186
+ // ==========================================================
187
+
188
+ /**
189
+ * 调度异步刷新。
190
+ *
191
+ * 使用 host.setTimeout(fn, 0) 在下一个微任务/宏任务中执行刷新。
192
+ * 如果已经调度过,则跳过。
193
+ */
194
+ private scheduleFlush(): void {
195
+ if (this.scheduled) return;
196
+ this.scheduled = true;
197
+ this.timerId = this.host.setTimeout(() => {
198
+ this.timerId = null;
199
+ this.scheduled = false;
200
+ this.flush();
201
+ }, 0);
202
+ }
203
+
204
+ /**
205
+ * 执行队列中所有操作。
206
+ */
207
+ private flush(): void {
208
+ if (this.flushing || this.queue.length === 0) return;
209
+
210
+ this.flushing = true;
211
+
212
+ // 如果队列未排序,在 flush 时统一排序一次
213
+ if (this.dirty) {
214
+ this.sortQueue();
215
+ this.dirty = false;
216
+ }
217
+
218
+ // 取出当前所有操作(防止 flush 过程中新增操作导致无限循环)
219
+ const ops = this.queue;
220
+ this.queue = [];
221
+
222
+ for (const op of ops) {
223
+ this.executeOperation(op);
224
+ }
225
+
226
+ this.flushing = false;
227
+
228
+ // 如果 flush 过程中又有新操作入队,继续调度
229
+ if (this.queue.length > 0) {
230
+ this.scheduleFlush();
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 执行单个渲染操作。
236
+ * FIX: P2-50 添加注释说明:insert / remove / move / patch 操作由上层 renderer 处理,
237
+ * 此处仅作为调度容器,实际执行通过 custom 类型传入
238
+ */
239
+ private executeOperation(op: RenderOperation): void {
240
+ switch (op.type) {
241
+ case 'custom':
242
+ op.fn();
243
+ break;
244
+ // insert / remove / move / patch 操作由上层 renderer 处理,
245
+ // 此处仅作为调度容器,实际执行通过 custom 类型传入
246
+ // 这些操作在 RenderQueue 中主要用于合并和排序,实际 DOM 操作在 renderer 层执行
247
+ case 'insert':
248
+ case 'remove':
249
+ case 'move':
250
+ case 'patch':
251
+ // 这些操作类型在 RenderQueue 中仅用于队列管理(合并、排序),
252
+ // 实际的 DOM 操作由上层 renderer 通过 custom 类型包装后传入
253
+ // 设计意图:RenderQueue 专注于调度,不直接操作 DOM
254
+ // FIX: P2-v11-22 在 DEV 模式下添加警告,提醒开发者这些操作类型不应直接执行
255
+ if (__DEV__) {
256
+ console.warn(
257
+ `[lytjs/render-queue] Operation type "${op.type}" should be wrapped in a "custom" operation. ` +
258
+ `Direct ${op.type} operations in RenderQueue are no-ops.`,
259
+ );
260
+ }
261
+ break;
262
+ default:
263
+ // 未知操作类型,静默忽略(防御性编程)
264
+ break;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * 尝试合并重复操作。
270
+ *
271
+ * 合并规则:
272
+ * - 同一元素的 remove 操作只保留最后一个
273
+ * - 同一元素的 custom 操作合并为一次
274
+ * - patch 操作:如果新旧 VNode 的 type 相同,仅保留最新的 patch
275
+ */
276
+ private tryMerge(newOp: RenderOperation): void {
277
+ const targetKey = this.getOperationKey(newOp);
278
+ if (!targetKey) {
279
+ this.queue.push(newOp);
280
+ return;
281
+ }
282
+
283
+ // 从后向前查找可合并的操作
284
+ for (let i = this.queue.length - 1; i >= 0; i--) {
285
+ const existing = this.queue[i]!;
286
+ if (this.getOperationKey(existing) === targetKey) {
287
+ // remove 操作:替换为最新的
288
+ if (newOp.type === 'remove' && existing.type === 'remove') {
289
+ this.queue[i] = newOp;
290
+ return;
291
+ }
292
+ // patch 操作:替换为最新的
293
+ if (newOp.type === 'patch' && existing.type === 'patch') {
294
+ this.queue[i] = newOp;
295
+ return;
296
+ }
297
+ // custom 操作:合并(保留两者,custom 不做去重)
298
+ }
299
+ }
300
+
301
+ this.queue.push(newOp);
302
+ }
303
+
304
+ /**
305
+ * 获取操作的标识 key,用于合并判断。
306
+ *
307
+ * 对于组件类型(非字符串 type),使用 vnode.key 或 type.name 避免不同组件被错误合并。
308
+ * FIX: P1-49 定义正确类型替代 any,使用 unknown 和类型守卫
309
+ * FIX: P2-49 提取 typeKey 为独立方法,避免每次调用时重复创建闭包
310
+ */
311
+ private getOperationKey(op: RenderOperation): string | null {
312
+ switch (op.type) {
313
+ case 'insert':
314
+ return `insert:${this.getVNodeTypeKey(op.vnode)}`;
315
+ case 'remove':
316
+ return `remove:${this.getVNodeTypeKey(op.vnode)}`;
317
+ case 'move':
318
+ return `move:${this.getVNodeTypeKey(op.vnode)}`;
319
+ case 'patch':
320
+ return `patch:${this.getVNodeTypeKey(op.newVNode)}`;
321
+ case 'custom':
322
+ return null;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * 获取 VNode 的类型 key,用于操作合并判断。
328
+ * FIX: P2-49 提取为独立方法,避免在 getOperationKey 中重复创建闭包
329
+ */
330
+ // FIX: DTS build error - 使用 unknown 中间类型
331
+ private getVNodeTypeKey(vnode: unknown): string {
332
+ const v = vnode as Record<string, unknown> | undefined;
333
+ const t = v?.type;
334
+ if (typeof t === 'string') {
335
+ return t;
336
+ }
337
+ // 组件类型:优先使用 vnode.key,其次使用 type.name 或 Symbol
338
+ if (v?.key != null) {
339
+ return String(v.key);
340
+ }
341
+ if (typeof t === 'function') {
342
+ return (t as { name?: string }).name ?? String(t);
343
+ }
344
+ if (typeof t === 'object' && t != null) {
345
+ const tObj = t as Record<string, unknown>;
346
+ return tObj.name != null ? String(tObj.name) : String(t);
347
+ }
348
+ return String(t);
349
+ }
350
+
351
+ /**
352
+ * 按优先级排序队列。
353
+ *
354
+ * 优先级高的排在前面,同优先级保持插入顺序(稳定排序)。
355
+ * FIX: P2-42 渲染队列优先级排序:确保 sync 优先级任务优先执行
356
+ * FIX: P2-48 添加第二排序键(插入索引)确保排序稳定性
357
+ */
358
+ private sortQueue(): void {
359
+ // 为每个操作添加原始索引作为第二排序键
360
+ const indexedQueue = this.queue.map((op, index) => ({ op, index }));
361
+ indexedQueue.sort((a, b) => {
362
+ const priorityA = this.getPriority(a.op);
363
+ const priorityB = this.getPriority(b.op);
364
+ const weightA = RENDER_PRIORITY_WEIGHT[priorityA];
365
+ const weightB = RENDER_PRIORITY_WEIGHT[priorityB];
366
+ // 优先级不同时按优先级排序,相同时按原始索引排序(保持插入顺序)
367
+ if (weightA !== weightB) {
368
+ return weightA - weightB;
369
+ }
370
+ return a.index - b.index;
371
+ });
372
+ this.queue = indexedQueue.map((item) => item.op);
373
+ }
374
+
375
+ /**
376
+ * 获取操作的优先级。
377
+ */
378
+ private getPriority(op: RenderOperation): RenderPriority {
379
+ return op.priority ?? this.options.defaultPriority;
380
+ }
381
+ }