Skip to content

Commit 8e79c2f

Browse files
committed
#FIX: destroy函数并未停止所有图片加载、绘制。
1 parent 7bf30a4 commit 8e79c2f

5 files changed

Lines changed: 102 additions & 87 deletions

File tree

src/ScrollFrameCanvas.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
8181

8282
private _boundOnScroll: () => void;
8383

84-
constructor(options: ScrollFrameCanvasOptions) {
84+
constructor (options: ScrollFrameCanvasOptions) {
8585
super();
8686
if (!options || !options.canvasEle) throw new Error('canvasEle 必填');
8787
if (!options.scrollbarEle) throw new Error('scrollbarEle 必填');
@@ -148,7 +148,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
148148
}
149149

150150
/** 启动 */
151-
async init(): Promise<this> {
151+
async init (): Promise<this> {
152152
const total = this.getTotalFrames();
153153
this._preRenderedFrames = new Array(total);
154154

@@ -245,7 +245,6 @@ export default class ScrollFrameCanvas extends EventEmitter {
245245
}
246246

247247

248-
249248
// 恢复滚动
250249
this._enableScroll();
251250

@@ -262,14 +261,14 @@ export default class ScrollFrameCanvas extends EventEmitter {
262261
}
263262

264263
/** 开始播放(启用渲染循环) */
265-
play(): void {
264+
play (): void {
266265
if (this._playing) return;
267266
this._playing = true;
268267
this._tick();
269268
}
270269

271270
/** 暂停播放 */
272-
pause(): void {
271+
pause (): void {
273272
this._playing = false;
274273
if (this._rAFId) {
275274
cancelAnimationFrame(this._rAFId);
@@ -278,32 +277,32 @@ export default class ScrollFrameCanvas extends EventEmitter {
278277
}
279278

280279
/** 停止并重置到 0 帧 */
281-
stop(): void {
280+
stop (): void {
282281
this.pause();
283282
this.setFrame(0);
284283
}
285284

286285
/**
287286
* 设置当前帧
288287
*/
289-
setFrame(frameIndex: number): void {
288+
setFrame (frameIndex: number): void {
290289
const idx = clamp(Math.round(frameIndex), 0, this.getTotalFrames() - 1);
291290
this._currentFrame = idx;
292291
this._renderIfNeeded();
293292
}
294293

295294
/** 获取当前帧索引 */
296-
getCurrentFrame(): number {
295+
getCurrentFrame (): number {
297296
return this._currentFrame;
298297
}
299298

300299
/** 获取总帧数 */
301-
getTotalFrames(): number {
300+
getTotalFrames (): number {
302301
return this.total;
303302
}
304303

305304
/** 销毁并清理资源 */
306-
destroy(): void {
305+
destroy (): void {
307306
if (this._destroyed) return;
308307
this.pause();
309308
this._detachScroll();
@@ -328,7 +327,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
328327
this._destroyed = true;
329328
}
330329

331-
private _cleanupNullFrames(): void {
330+
private _cleanupNullFrames (): void {
332331
// 移除 null 元素,重新排列数组
333332
const validFrames: ImageBitmap[] = [];
334333
const originalLength = this._preRenderedFrames.length;
@@ -354,6 +353,9 @@ export default class ScrollFrameCanvas extends EventEmitter {
354353
index: number, offscreenCanvas: OffscreenCanvas, offscreenCtx: OffscreenCanvasRenderingContext2D,
355354
isBack = false,
356355
): Promise<void> {
356+
if (this._destroyed) {
357+
throw new Error('ScrollFrameCanvas 已销毁');
358+
}
357359
try {
358360
const image = await this._loadFrame(index);
359361

@@ -379,7 +381,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
379381
}
380382
}
381383

382-
private async _parseImageData(index: number, offscreenCanvas: OffscreenCanvas, offscreenCtx: OffscreenCanvasRenderingContext2D, image: ImageBitmap | HTMLImageElement): Promise<void> {
384+
private async _parseImageData (index: number, offscreenCanvas: OffscreenCanvas, offscreenCtx: OffscreenCanvasRenderingContext2D, image: ImageBitmap | HTMLImageElement): Promise<void> {
383385
try {
384386
if (image instanceof ImageBitmap) {
385387
this._preRenderedFrames[index] = image;
@@ -399,7 +401,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
399401
}
400402
}
401403

402-
private async _loadFrame(index: number): Promise<HTMLImageElement | ImageBitmap | null> {
404+
private async _loadFrame (index: number): Promise<HTMLImageElement | ImageBitmap | null> {
403405
const image = await this._getImage(index);
404406
if (image === null) {
405407
return null;
@@ -410,7 +412,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
410412
return image;
411413
}
412414

413-
private async _getImage(index: number): Promise<HTMLImageElement | ImageBitmap | string | null> {
415+
private async _getImage (index: number): Promise<HTMLImageElement | ImageBitmap | string | null> {
414416
if (this.images) {
415417
if (this.images[index] === undefined) {
416418
throw new Error(`images 数组索引 ${index} 对应的图片为 undefined`);
@@ -429,7 +431,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
429431
return result;
430432
}
431433

432-
private _loadImageFromURL(url: string): Promise<HTMLImageElement> {
434+
private _loadImageFromURL (url: string): Promise<HTMLImageElement> {
433435
return new Promise((resolve, reject) => {
434436
const img = new Image();
435437
img.crossOrigin = 'anonymous';
@@ -439,7 +441,8 @@ export default class ScrollFrameCanvas extends EventEmitter {
439441
});
440442
}
441443

442-
private _drawImageToOffscreen(img: HTMLImageElement | ImageBitmap, ctx: OffscreenCanvasRenderingContext2D, canvasW: number, canvasH: number): void {
444+
private _drawImageToOffscreen (img: HTMLImageElement | ImageBitmap, ctx: OffscreenCanvasRenderingContext2D, canvasW: number, canvasH: number): void {
445+
if (this._destroyed) return;
443446
const natW = (img as HTMLImageElement).naturalWidth || img.width || 1;
444447
const natH = (img as HTMLImageElement).naturalHeight || img.height || 1;
445448

@@ -448,7 +451,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
448451
ctx.drawImage(img, 0, 0, natW, natH, Math.round(fit.x), Math.round(fit.y), Math.round(fit.w), Math.round(fit.h));
449452
}
450453

451-
private _disableScroll(): void {
454+
private _disableScroll (): void {
452455
const el = this.scrollbarEle;
453456
if (el === window || el === document || el === document.body) {
454457
// 保存原始滚动行为
@@ -474,7 +477,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
474477
}
475478
}
476479

477-
private _enableScroll(): void {
480+
private _enableScroll (): void {
478481
const el = this.scrollbarEle;
479482
if (el === window || el === document || el === document.body) {
480483
// 恢复原始滚动行为
@@ -489,7 +492,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
489492
}
490493
}
491494

492-
private _attachScroll(): void {
495+
private _attachScroll (): void {
493496
const el = this.scrollbarEle;
494497
if (!el) return;
495498
const opts = { passive: true };
@@ -502,7 +505,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
502505
}
503506
}
504507

505-
private _detachScroll(): void {
508+
private _detachScroll (): void {
506509
const el = this.scrollbarEle;
507510
if (!el) return;
508511
if (el === window || el === document) {
@@ -515,7 +518,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
515518
}
516519

517520

518-
private _getScrollPixels(): { scrollPixels: number; totalScrollPixels: number } {
521+
private _getScrollPixels (): { scrollPixels: number; totalScrollPixels: number } {
519522
const el = this.scrollbarEle;
520523
const axis = this.options.scrollAxis;
521524

@@ -541,13 +544,13 @@ export default class ScrollFrameCanvas extends EventEmitter {
541544
}
542545
}
543546

544-
private _handleScroll(): void {
547+
private _handleScroll (): void {
545548
if (!this._playing) return;
546549
// 只更新帧索引,不直接绘制
547550
this._currentFrame = this._calcFrameFromScroll();
548551
}
549552

550-
private _tick(): void {
553+
private _tick (): void {
551554
if (!this._playing) return;
552555
this._rAFId = requestAnimationFrame(() => {
553556
// 检查当前帧是否需要绘制
@@ -569,14 +572,15 @@ export default class ScrollFrameCanvas extends EventEmitter {
569572
});
570573
}
571574

572-
private _calcFrameFromScroll(): number {
575+
private _calcFrameFromScroll (): number {
573576
const total = this.getTotalFrames();
574577
const { scrollPixels, totalScrollPixels } = this._getScrollPixels();
575578
const frameIndex = this.options.frameMapper(scrollPixels, totalScrollPixels, total);
576579
return clamp(Math.round(frameIndex), 0, total - 1);
577580
}
578581

579582
private _renderIfNeeded (force: boolean = false, usePrevRealFrame = false) {
583+
if (this._destroyed) return;
580584
const frame = this._currentFrame;
581585
if (!force) {
582586
if (frame === this._lastDrawnFrame) {
@@ -628,6 +632,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
628632
}
629633

630634
private _drawPreRenderedFrame (preRenderedFrame: ImageBitmap): void {
635+
if (this._destroyed) return;
631636
if (!this.ctx) return;
632637

633638
const ctx = this.ctx;
@@ -638,7 +643,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
638643
ctx.drawImage(preRenderedFrame, 0, 0, canvasW, canvasH);
639644
}
640645

641-
private _startLoadingAnimation() {
646+
private _startLoadingAnimation () {
642647
this._loadingProgress = 0;
643648
this._loadingProgressCount = 0;
644649
this._loadingCompleted = false;
@@ -647,11 +652,12 @@ export default class ScrollFrameCanvas extends EventEmitter {
647652
private _stopLoadingAnimation (): void {
648653
this._loadingCompleted = true;
649654
cancelAnimationFrame(this._loadingAnimationId);
650-
this._loadingAnimationId = 0;
651-
// 清空画布
652-
if (this.ctx) {
653-
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
654-
}
655+
this._loadingAnimationId = 0;
656+
if (this._destroyed) return;
657+
// 清空画布
658+
if (this.ctx) {
659+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
660+
}
655661
}
656662

657663
private _updateLoadingProgress (): void {
@@ -667,7 +673,8 @@ export default class ScrollFrameCanvas extends EventEmitter {
667673
this._loadingAnimationId = requestAnimationFrame(animate);
668674
}
669675

670-
private _drawLoadingAnimation(): void {
676+
private _drawLoadingAnimation (): void {
677+
if (this._destroyed) return;
671678
if (this._loadingCompleted) return;
672679
if (!this.ctx) return;
673680

@@ -760,6 +767,7 @@ export default class ScrollFrameCanvas extends EventEmitter {
760767
}
761768

762769
private _drawBackgroundLoadingAnimation (): void {
770+
if (this._destroyed) return;
763771
if (this._backgroundLoadingCompleted || !this.ctx) return;
764772
if (!this.options.large?.showBackgroundProgress) return;
765773
this._renderIfNeeded(true, true);

src/demo.ts

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ let scrollHint: HTMLElement | null = null;
77
let hintShown = false;
88

99
// 显示滚动提示
10-
function showScrollHint(): void {
10+
function showScrollHint (): void {
1111
if (scrollHint && !hintShown) {
1212
scrollHint.style.opacity = '1';
1313
hintShown = true;
1414
}
1515
}
1616

1717
// 隐藏滚动提示
18-
function hideScrollHint(): void {
18+
function hideScrollHint (): void {
1919
if (scrollHint && hintShown) {
2020
scrollHint.style.opacity = '0';
2121
hintShown = false;
@@ -35,7 +35,7 @@ async function initScrollFrameCanvas (): Promise<void> {
3535
const imageFactory = async (index: number) => {
3636
if (currentFrameRatio !== 0) {
3737
if (index % currentFrameRatio !== 0) {
38-
return null;
38+
return null;
3939
}
4040
}
4141
return `${base}${String(index + 1).padStart(3, '0')}.jpg`;
@@ -63,11 +63,11 @@ async function initScrollFrameCanvas (): Promise<void> {
6363
},
6464
frameMapper: (scrollPixels: number) => {
6565
// console.log('totalFrames:', totalFrames);
66-
return Math.round(scrollPixels / (9 * Math.max(1, currentFrameRatio)));
66+
return Math.round(scrollPixels / (9 * Math.max(1, currentFrameRatio)));
6767
},
6868
});
6969

70-
const beginSecond = Date.now() / 1000;
70+
const beginSecond = Date.now() / 1000;
7171
await scrollFrameCanvas.init();
7272
const endSecond = Date.now() / 1000;
7373
console.log('初始化完成,耗时:', endSecond - beginSecond, '秒');
@@ -84,22 +84,22 @@ async function rerender (): Promise<void> {
8484
const rerenderBtn = document.getElementById('rerenderBtn') as HTMLButtonElement;
8585

8686

87-
try {
88-
// 禁用按钮
89-
rerenderBtn.disabled = true;
90-
rerenderBtn.textContent = '渲染中...';
87+
try {
88+
// 禁用按钮
89+
rerenderBtn.disabled = true;
90+
rerenderBtn.textContent = '渲染中...';
9191

92-
// 隐藏提示
93-
hideScrollHint();
92+
// 隐藏提示
93+
hideScrollHint();
9494

95-
// 销毁当前实例
96-
if (scrollFrameCanvas) {
97-
scrollFrameCanvas.destroy();
98-
scrollFrameCanvas = null;
99-
}
95+
// 销毁当前实例
96+
if (scrollFrameCanvas) {
97+
scrollFrameCanvas.destroy();
98+
scrollFrameCanvas = null;
99+
}
100100

101-
// 重新初始化
102-
await initScrollFrameCanvas();
101+
// 重新初始化
102+
await initScrollFrameCanvas();
103103

104104
} catch (error) {
105105
console.error('重新渲染失败:', error);

0 commit comments

Comments
 (0)