From 3fe9cf9b664dc5b39c17b40efe1d3935c89f7553 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 28 Jul 2026 15:32:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E4=BF=AE=E5=A4=8D=20IdleTask=20?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=BC=82=E5=B8=B8=E6=88=96=20clearTasks=20?= =?UTF-8?q?=E6=97=B6=E9=98=9F=E5=88=97=E5=8D=A1=E4=BD=8F=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单个任务失败不再中断整批依赖收集,并在 runTaskQueue 中正确重置 taskHandle、始终通过实例队列消费任务。 --- packages/editor/src/utils/dep/idle-task.ts | 113 +++++++++++------- .../editor/tests/unit/services/dep.spec.ts | 23 ++++ .../tests/unit/utils/dep/idle-task.spec.ts | 72 +++++++++++ 3 files changed, 167 insertions(+), 41 deletions(-) diff --git a/packages/editor/src/utils/dep/idle-task.ts b/packages/editor/src/utils/dep/idle-task.ts index 7ef8de6c..7051d3f0 100644 --- a/packages/editor/src/utils/dep/idle-task.ts +++ b/packages/editor/src/utils/dep/idle-task.ts @@ -1,5 +1,7 @@ import { EventEmitter } from 'events'; +import { error } from '../logger'; + export interface IdleTaskEvents { finish: []; 'hight-level-finish': []; @@ -64,7 +66,7 @@ export class IdleTask extends EventEmitter { this.taskHandle = null; this.emit('update-task-length', { - length: this.taskList.length + this.hightLevelTaskList.length, + length: this.getTaskLength(), hightLevelLength: this.hightLevelTaskList.length, }); } @@ -88,51 +90,80 @@ export class IdleTask extends EventEmitter { } private runTaskQueue(deadline: IdleDeadline) { - const { hightLevelTaskList, taskList } = this; + // 本次回调已触发,句柄随之失效;置空后 clearTasks / enqueueTask 才能正确判断是否需要重新调度 + this.taskHandle = null; - // 动画会占用空闲时间,当任务一直无法执行时,看看是否有动画正在播放 - // 根据空闲时间的多少来决定执行的任务数,保证页面不卡死的情况下尽量多执行任务,不然当任务数巨大时,执行时间会很久 - // 执行不完不会影响配置,但是会影响画布渲染 - while (deadline.timeRemaining() > 0 && (taskList.length || hightLevelTaskList.length)) { - const timeRemaining = deadline.timeRemaining(); - let times = 0; - if (timeRemaining <= 5) { - times = 10; - } else if (timeRemaining <= 10) { - times = 100; - } else if (timeRemaining <= 15) { - times = 300; + try { + // 动画会占用空闲时间,当任务一直无法执行时,看看是否有动画正在播放 + // 根据空闲时间的多少来决定执行的任务数,保证页面不卡死的情况下尽量多执行任务,不然当任务数巨大时,执行时间会很久 + // 执行不完不会影响配置,但是会影响画布渲染 + while (deadline.timeRemaining() > 0 && this.getTaskLength()) { + const timeRemaining = deadline.timeRemaining(); + let times = 0; + if (timeRemaining <= 5) { + times = 10; + } else if (timeRemaining <= 10) { + times = 100; + } else if (timeRemaining <= 15) { + times = 300; + } else { + times = 600; + } + + for (let i = 0; i < times; i++) { + // 每次都从实例上取队列,任务执行过程中调用 clearTasks 能立即生效,不会继续消费已被清空的旧队列 + const task = this.hightLevelTaskList.length > 0 ? this.hightLevelTaskList.shift() : this.taskList.shift(); + if (task) { + this.runTask(task); + } + + if (!this.getTaskLength()) { + break; + } + } + } + } finally { + // 必须放在 finally 中:一旦这里被跳过,taskHandle 会一直是真值, + // enqueueTask 也就不会再重新调度,剩余任务与任务数将永久停在当前状态 + this.finishRun(); + } + } + + /** + * 单个任务失败不能中断整个队列,否则后续任务永远不会被执行, + * 依赖收集会停在半路(收集中状态与剩余任务数都不再变化) + */ + private runTask(task: TaskList[number]) { + try { + task.handler(task.data); + } catch (e) { + error('magic editor: 空闲任务执行失败', e); + } + } + + private finishRun() { + try { + if (!this.hightLevelTaskList.length) { + this.emit('hight-level-finish'); + } + } finally { + if (this.getTaskLength()) { + // 任务执行或事件监听中可能已经重新调度过 + if (!this.taskHandle) { + this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 300 }); + } } else { - times = 600; + this.emit('finish'); } - for (let i = 0; i < times; i++) { - const task = hightLevelTaskList.length > 0 ? hightLevelTaskList.shift() : taskList.shift(); - if (task) { - task.handler(task.data); - } - - if (hightLevelTaskList.length === 0 && taskList.length === 0) { - break; - } - } + this.emit('update-task-length', { + length: this.getTaskLength(), + hightLevelLength: this.hightLevelTaskList.length, + }); } + } - if (!hightLevelTaskList.length) { - this.emit('hight-level-finish'); - } - - if (hightLevelTaskList.length || taskList.length) { - this.taskHandle = globalThis.requestIdleCallback(this.runTaskQueue.bind(this), { timeout: 300 }); - } else { - this.taskHandle = 0; - - this.emit('finish'); - } - - this.emit('update-task-length', { - length: taskList.length + hightLevelTaskList.length, - hightLevelLength: hightLevelTaskList.length, - }); + private getTaskLength() { + return this.taskList.length + this.hightLevelTaskList.length; } } diff --git a/packages/editor/tests/unit/services/dep.spec.ts b/packages/editor/tests/unit/services/dep.spec.ts index 9a796b74..09744f9c 100644 --- a/packages/editor/tests/unit/services/dep.spec.ts +++ b/packages/editor/tests/unit/services/dep.spec.ts @@ -297,6 +297,29 @@ describe('Dep service', () => { expect(depService.get('collecting')).toBe(false); }); + test('target 收集抛错时批次仍会结算,collecting 与 taskLength 复位', async () => { + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined); + depService.addTarget( + new Target({ + id: 'throw-target', + type: DepTargetType.DATA_SOURCE, + isTarget: () => { + throw new Error('boom'); + }, + }), + ); + + const nodes = Array.from({ length: 50 }, (_, i) => ({ id: `n${i}`, type: 'text', text: 'x' })) as any; + await expect(depService.collectIdle(nodes, {}, false, DepTargetType.DATA_SOURCE)).resolves.toBe(true); + + expect(depService.get('collecting')).toBe(false); + // taskLength 的更新做了 1s 节流,等节流窗口结束后才会同步到 0 + await new Promise((resolve) => setTimeout(resolve, 1100)); + expect(depService.get('taskLength')).toBe(0); + + errorSpy.mockRestore(); + }); + test('destroy 会 reset 并移除监听', () => { depService.addTarget(makeTarget('destroy-me')); expect(() => depService.destroy()).not.toThrow(); diff --git a/packages/editor/tests/unit/utils/dep/idle-task.spec.ts b/packages/editor/tests/unit/utils/dep/idle-task.spec.ts index 7d42f1ed..1a442793 100644 --- a/packages/editor/tests/unit/utils/dep/idle-task.spec.ts +++ b/packages/editor/tests/unit/utils/dep/idle-task.spec.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { IdleTask } from '@editor/utils/dep/idle-task'; +import * as logger from '@editor/utils/logger'; const fakeIdleDeadline = (timeRemaining: number, callsBeforeZero = 1): IdleDeadline => { let remainingCalls = callsBeforeZero; @@ -127,6 +128,77 @@ describe('IdleTask', () => { expect(scheduled.length).toBeGreaterThan(1); }); + test('单个任务抛错不会中断整个队列', () => { + const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined); + const task = new IdleTask(); + const done: number[] = []; + const finishHandler = vi.fn(); + task.on('finish', finishHandler); + + for (let i = 0; i < 10; i++) { + task.enqueueTask((n) => { + if (n === 3) throw new Error('boom'); + done.push(n); + }, i); + } + + expect(() => scheduled[0].cb(fakeIdleDeadline(50))).not.toThrow(); + expect(done).toEqual([0, 1, 2, 4, 5, 6, 7, 8, 9]); + expect(finishHandler).toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalled(); + errorSpy.mockRestore(); + }); + + test('任务抛错且仍有剩余任务时继续调度下一轮并同步任务数', () => { + const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => undefined); + const task = new IdleTask(); + const updateHandler = vi.fn(); + task.on('update-task-length', updateHandler); + + for (let i = 0; i < 200; i++) { + task.enqueueTask((n) => { + if (n === 0) throw new Error('boom'); + }, i); + } + + // 单批最多执行 10 个,执行完仍有剩余任务 + scheduled[0].cb(fakeIdleDeadline(3, 1)); + + expect(scheduled.length).toBeGreaterThan(1); + expect(updateHandler).toHaveBeenCalledWith({ length: 190, hightLevelLength: 0 }); + errorSpy.mockRestore(); + }); + + test('任务执行过程中 clearTasks 会立即停止消费已清空的队列', () => { + const task = new IdleTask(); + const handler = vi.fn((n: number) => { + if (n === 0) { + task.clearTasks(); + } + }); + for (let i = 0; i < 200; i++) task.enqueueTask(handler, i); + + scheduled[0].cb(fakeIdleDeadline(50)); + + expect(handler).toHaveBeenCalledTimes(1); + }); + + test('任务执行过程中清空并重新入队,新任务仍会被执行', () => { + const task = new IdleTask(); + const afterHandler = vi.fn(); + task.enqueueTask((n) => { + if (n === 0) { + task.clearTasks(); + task.enqueueTask(afterHandler, 999); + } + }, 0); + for (let i = 1; i < 200; i++) task.enqueueTask(() => undefined, i); + + scheduled[0].cb(fakeIdleDeadline(50)); + + expect(afterHandler).toHaveBeenCalled(); + }); + test('clearTasks - 取消挂起任务并重置队列', () => { const task = new IdleTask(); task.enqueueTask(() => undefined, 1);