Angular结构指令如何确保移除骨架屏后再渲染目标组件
Angular骨架屏结构指令生命周期异常解决方案
问题复现
自定义骨架屏结构指令实现代码如下:
import { Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appSkeleton]', }) export class SkeletonDirective implements OnChanges { @Input('appSkeleton') isLoading: boolean; @Input('appSkeletonSkeletonTemplate') skeletonTemplate: TemplateRef<any>; constructor(private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef) {} ngOnChanges(changes: SimpleChanges) { if (changes.isLoading) { this.viewContainer.clear(); if (changes.isLoading.currentValue) { this.viewContainer.createEmbeddedView(this.skeletonTemplate); } else { this.viewContainer.createEmbeddedView(this.templateRef); } } } }
指令使用方式:
<app-some-component *appSkeleton="loading; skeletonTemplate:someComponentSkeleton"></app-some-component>
问题描述
加载状态切换时,挂载指令的目标组件生命周期钩子会在骨架屏与主组件同时存在于DOM的时刻触发,引发异常,典型场景为ngAfterViewInit钩子提前执行。
曾尝试将this.viewContainer.createEmbeddedView(this.templateRef);包裹在setTimeout(() => {}, 0)中,虽然能解决生命周期提前触发的问题,但会导致DOM短暂出现既无骨架屏也无目标组件的空窗期,产生白屏闪烁,无法满足需求。
核心诉求:组件模板仅在骨架屏完全从DOM移除后才渲染挂载,且无白屏空窗。
解决方案
问题根源:Angular视图更新为批量同步执行,原实现中调用clear()后立即创建新嵌入视图时,旧视图(骨架屏)的销毁和新视图(业务组件)的初始化会在同一个变更检测周期内完成,两个视图的DOM会短暂共存,导致业务组件生命周期提前触发。而setTimeout方案将新视图创建逻辑放到下一个宏任务执行,当前宏任务结束后浏览器会先渲染一次空DOM状态,必然出现白屏。
利用JS线程与浏览器渲染线程互斥的特性,基于Angular视图容器原生API实现同步无缝切换,修复后代码如下:
import { Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef, EmbeddedViewRef } from '@angular/core'; @Directive({ selector: '[appSkeleton]', }) export class SkeletonDirective implements OnChanges { @Input('appSkeleton') isLoading: boolean; @Input('appSkeletonSkeletonTemplate') skeletonTemplate: TemplateRef<any>; private currentView: EmbeddedViewRef<any> | null = null; constructor( private templateRef: TemplateRef<any>, private viewContainer: ViewContainerRef ) {} ngOnChanges(changes: SimpleChanges) { if (!changes.isLoading) return; const isLoading = changes.isLoading.currentValue; // 首次加载直接渲染对应视图 if (changes.isLoading.firstChange) { this.currentView = this.viewContainer.createEmbeddedView( isLoading ? this.skeletonTemplate : this.templateRef ); return; } // 从内容态切到加载态:直接替换即可,业务组件销毁无异常问题 if (isLoading) { this.viewContainer.clear(); this.currentView = this.viewContainer.createEmbeddedView(this.skeletonTemplate); return; } // 从加载态切到内容态:核心处理逻辑,避免视图共存和白屏 // 1. 同步从容器中拆离骨架屏视图,DOM节点同步移除,暂不销毁实例 const skeletonViewIndex = this.viewContainer.indexOf(this.currentView); this.viewContainer.detach(skeletonViewIndex); // 2. 同步创建并插入业务组件视图,此时DOM中已无骨架屏节点 const contentView = this.viewContainer.createEmbeddedView(this.templateRef); // 3. 销毁骨架屏视图释放内存 this.currentView.destroy(); this.currentView = contentView; } }
方案优势
- 无生命周期异常:骨架屏DOM节点同步移除后才会创建业务组件视图,业务组件生命周期触发时DOM中不存在骨架屏,彻底解决钩子提前执行问题
- 无白屏空窗:拆离旧视图、创建插入新视图的逻辑全部在同一个同步执行流中完成,浏览器只会在整个逻辑执行完毕后触发一次重绘,用户看不到中间空状态,无闪烁
- 无额外依赖:全部基于Angular视图容器原生API实现,不需要手动操作DOM,兼容多根节点组件场景
- 无内存泄漏:旧的骨架屏视图在新视图插入后会被主动销毁,内存正常释放
内容的提问来源于stack exchange,提问作者Vladislav Kochurko
相关产品推荐
相关产品推荐

