Angular点击卡片触发randomSize()重渲染 瓦片高度随机变动问题
Angular默认开发模式下,每次触发变更检测(点击事件、异步回调、路由变更等都会触发),都会重新执行模板中绑定的所有方法、表达式,校验返回值是否发生变化。你当前直接在模板的[maxHeight]绑定中调用randomSize()方法,每次变更检测运行时,该方法都会重新生成新的随机值传给卡片组件,直接导致所有卡片高度无规律重置。
你在CardComponent中写的ngOnChanges判断firstChange的逻辑也存在缺陷:首次赋值后直接return,会拦截后续所有合法的输入属性更新,而且完全无法解决父组件每次传入新随机值的根本问题。
Angular模板绑定的核心原则是:不要在模板中调用返回值不稳定的方法(比如带随机数、依赖可变全局状态的方法),所有需要动态计算的绑定值,都要在数据初始化阶段计算完成后存入变量,模板仅绑定预计算好的固定值。
1. 预生成每个卡片的固定高度
在父组件拿到分类列表数据时,就给每个分类项提前生成一次随机高度并缓存,避免每次变更检测重复计算。
修改DomainDetailsComponent.ts中的数据加载逻辑:
this.domain$ = this.domainService.getDomainById(this.domainId).pipe(map((res) => { let domain = res.data[0]; if (domain && domain.categories?.length) { // 遍历分类,每个分类只生成一次随机高度并挂载到对象上 domain.categories.forEach(category => { (category as any).fixedHeight = this.randomSize(); }); this.divideArray(domain.categories, domain.categories.length / 3); } return domain }));
如果不想修改原有CategoryModel的类型定义,可以单独创建一个以分类ID为key的Map对象存储高度:
// 在类中声明映射表 cardHeightMap = new Map<number, number>(); // 数据加载时生成高度存入映射表 domain.categories.forEach(category => { this.cardHeightMap.set(category.id, this.randomSize()); });
2. 修改模板绑定,去掉方法调用
把模板中直接调用randomSize()的部分,替换为绑定预先生成好的固定高度值。
修改DomainDetailsComponent.html:
<div class="grid_row" *ngIf="catArray.length > 0"> <ng-container *ngFor="let line of catArray"> <div class="grid_column"> <!-- 直接绑定预生成的固定高度 --> <app-card *ngFor="let cat of line" [category]="cat" [maxWidth]="450" [maxHeight]="cat.fixedHeight" <!-- 如果用映射表就写[maxHeight]="cardHeightMap.get(cat.id)" --> ></app-card> </div> </ng-container> </div>
3. 清理CardComponent的冗余逻辑
@Input()装饰的属性本身会在父组件传值变化时自动更新,不需要手动在ngOnChanges中重新赋值,你当前写的ngOnChanges反而会拦截后续合法的属性更新,直接删除整个ngOnChanges方法即可。
清理后的CardComponent.ts核心代码:
import {Component, HostListener, Input, OnInit} from '@angular/core'; import {CategoryModel} from "../../models/CategoryModel"; @Component({ selector: 'app-card', templateUrl: './card.component.html', styleUrls: ['./card.component.scss'] }) export class CardComponent implements OnInit { @Input() category?: CategoryModel; @Input() descLimit?: number = 20; @Input() allowRedirect: boolean = true; @Input() maxWidth?: number = 250; @Input() maxHeight?: number = 300; constructor() {} ngOnInit(): void {} @HostListener("click", ["$event"]) public onClick(event: any): void { if (!this.allowRedirect) event.stopPropagation(); } }
- 即便在生产环境下Angular变更检测只会执行一轮,也不推荐在模板中绑定复杂方法调用,会造成不必要的性能损耗,所有需要计算的绑定值都要提前在组件类中缓存。
- 你当前用手动切分数组实现的多列瀑布流,在后续做响应式适配、无限加载、卡片排序时维护成本很高,可以考虑用成熟的瀑布流布局库实现。
内容的提问来源于stack exchange,提问作者alexandrebzk

