Angular4变更检测问题:NgFor内组件异常更新解决方案咨询
你的问题看起来是所有<app-kameroke>实例会同时更新,但根本原因不是变更检测的问题,而是下面两个关键错误导致的:
所有子组件的iframe都用了固定的
id="test",当你在子组件里通过document.getElementById("test")获取元素时,只会拿到DOM中第一个匹配该id的iframe,这意味着所有子组件的widget实际上都绑定到了同一个iframe的事件上。当这个iframe触发PLAY_PROGRESS事件时,所有子组件都会执行更新逻辑,导致所有实例的文本一起变化。你直接修改了
@Input装饰的属性(first/second/third),虽然字符串是值类型不会污染父组件数据,但结合第一个问题,会让所有子组件同步更新这些属性。
步骤1:给每个子组件分配唯一的iframe ID
在父组件的模板中,把每个lyric的id传递给子组件,作为iframe的唯一标识:
<!-- 修改后的父组件模板 --> <app-kameroke *ngFor="let lyric of lyrics" [first]="lyric.first" [second]="lyric.second" [third]="lyric.third" [uniqueId]="lyric.id" <!-- 新增:传递唯一ID --> ></app-kameroke>
步骤2:修改子组件代码
- 添加
uniqueId的输入属性 - 用这个唯一ID获取对应的iframe,避免多个组件绑定到同一个元素
- 把
@Input的属性从private改为public(模板需要访问这些属性,private会导致Angular模板无法读取) - 创建本地副本存储歌词内容,避免直接修改
@Input属性
修改后的子组件代码:
// 修改后的kamerokeComponent import { Component, OnInit, Input, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-kameroke', templateUrl: './kameroke.component.html', styleUrls: ['./kameroke.component.css'] }) export class KamerokeComponent implements OnInit { @Input() url: string; @Input() public initialFirst: string; // 重命名区分本地副本 @Input() public initialSecond: string; @Input() public initialThird: string; @Input() public uniqueId: string; // 新增:接收唯一ID // 本地副本,用于渲染和更新 public first: string; public second: string; public third: string; masked: boolean = true; playing: boolean = false; widget: any; lyrics: object; constructor(private ref: ChangeDetectorRef) { } ngOnInit() { // 初始化本地副本 this.first = this.initialFirst; this.second = this.initialSecond; this.third = this.initialThird; this.lyrics = { 1: "ca marche", 2: "ca cartonne", 3: "c le feu", 4: "testons voir" }; // 使用uniqueId获取当前组件的iframe this.widget = window['SC'].Widget(document.getElementById(this.uniqueId)); this.widget.bind(window['SC'].Widget.Events.PLAY, () => { this.playing = true }); this.widget.bind(window['SC'].Widget.Events.PAUSE, () => { this.playing = false }); this.widget.bind(window['SC'].Widget.Events.FINISH, () => { this.playing = false }); this.widget.bind(window['SC'].Widget.Events.PLAY_PROGRESS, () => { this.widget.getPosition((time) => { if (this.lyrics) { let timing = Math.ceil(time / 1000); if (this.lyrics[timing]) { console.log(timing); // 更新本地副本,而非@Input属性 this.first = this.lyrics[timing]; this.second = this.lyrics[timing]; this.third = this.lyrics[timing]; this.ref.detectChanges(); } } }); }); } hide() { this.masked = false; } }
步骤3:修改子组件模板
把iframe的id绑定到uniqueId,确保每个组件的iframe有唯一标识:
<!-- 修改后的子组件模板 --> <div class="container"> <div class="row"> <div class="col-sm soundcloudCol"> <iframe (load)="hide()" [attr.id]="uniqueId" <!-- 绑定唯一ID --> allow="autoplay" width="100%" height="100%" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=https://soundcloud.com/maahlox/la-vie-cest-la-bastonde&{ ADD YOUR PARAMETERS HERE }"> </iframe> </div> </div> <div class="row lyrics" [hidden]="masked" > <div class="col-sm"> <br><br> <p class="first">{{first}}</p> <p class="second">{{second}}</p> <p class="second">{{third}}</p> </div> </div> </div>
额外优化建议
避免直接操作DOM(
document.getElementById),可以用@ViewChild来获取当前组件内的iframe元素,更符合Angular的组件化思想:
在子组件模板给iframe加模板引用:<iframe #soundcloudIframe ...></iframe>
然后在组件里用@ViewChild('soundcloudIframe') soundcloudIframe: ElementRef;获取,代替document.getElementById。如果不需要手动触发变更检测,可以尝试移除
this.ref.detectChanges(),因为Angular通常会在事件回调后自动触发变更检测(除非你用了ChangeDetectionStrategy.OnPush)。
内容的提问来源于stack exchange,提问作者Anicet Nge

