Javascript 更改QueryList.changes上的ContentChildren模型
假设我有一个具有Javascript 更改QueryList.changes上的ContentChildren模型,javascript,angular,Javascript,Angular,假设我有一个具有@ContentChildren(Child)子级的父组件。假设每个子类在其组件类中都有一个索引字段。当父项的子项发生更改时,我希望使这些索引字段保持最新,具体操作如下: this.children.changes.subscribe(() => { this.children.forEach((child, index) => { child.index = index; }) }); 然而,当我尝试这样做时,我得到了一个“ExpressionCh
@ContentChildren(Child)子级的父组件。假设每个子类
在其组件类中都有一个索引
字段。当父项的子项发生更改时,我希望使这些索引字段保持最新,具体操作如下:
this.children.changes.subscribe(() => {
this.children.forEach((child, index) => {
child.index = index;
})
});
然而,当我尝试这样做时,我得到了一个“ExpressionChangedAfter…”错误,我猜这是因为索引更新发生在更改周期之外。下面是一个stackblitz演示此错误:
我怎样才能解决这个问题?一个明显的方法是简单地将索引绑定到模板中。第二个明显的方法是在更新其索引时为每个子级调用detectChanges()
。假设我不能使用这两种方法中的任何一种,还有其他方法吗?一种方法是使用宏任务更新索引值。这本质上是一个设置超时
,但请耐心听我说
这使您的StackBlitz订阅看起来像这样:
ngAfterContentInit(){
this.foos.changes.subscribe(()=>{
//宏任务
设置超时(()=>{
this.foos.forEach((foo,index)=>{
foo.index=索引;
});
}, 0);
});
}
这是一本书
因此javascript事件循环开始发挥作用。“ExpressionChangedAfter…”错误的原因是强调了正在对其他组件进行更改的事实,这本质上意味着应该运行另一个更改检测周期,否则您可能会在UI中得到不一致的结果。这是需要避免的
这归结起来就是,如果我们想要更新某些内容,但我们知道它不应该导致其他副作用,那么我们可以在宏任务队列中安排一些内容。更改检测过程完成后,才执行队列中的下一个任务
资源
整个事件循环都在javascript中,因为只有一个线程可以使用,所以了解发生了什么是很有用的
这更好地解释了Javascript事件循环,并深入到了微/宏队列的细节
为了更深入地了解和运行代码示例,我发现Jake Archibald的帖子非常好:使用下面的代码,在下一个周期中进行更改
this.foos.changes.subscribe(() => {
setTimeout(() => {
this.foos.forEach((foo, index) => {
foo.index = index;
});
});
});
这里的问题是,在视图生成过程进一步修改它试图首先显示的数据之后,您正在更改某些内容。理想的更改位置是在显示视图之前的生命周期钩子中,但是这里出现了另一个问题,即this.foos
是未定义的
,当这些钩子被称为QueryList时,仅在ngAfterContentInit
之前填充
不幸的是,目前没有太多的选择。详细解释微观/宏观任务对于理解hackysetTimeout
的工作原理非常有用
但是一个可观测的解决方案是使用更多的可观测/操作符(双关语),因此在我看来,管道延迟操作符是一个更干净的版本,因为setTimeout
被封装在其中
ngAfterContentInit(){
this.foos.changes.pipe(延迟(0)).subscribe(()=>{
this.foos.forEach((foo,index)=>{
foo.index=索引;
});
});
}
这里是如前所述,错误来自于更改周期评估后的值更改
{{index}}
更具体地说,视图使用本地组件变量索引来分配0
。。。当一个新项目被推送到数组中时,它会被更改。。。只有在创建了上一个项目并将其添加到DOM中,且索引值为0
之后,您的订阅才会为该项目设置真正的索引
setTimout
或.pipe(delay(0))
(本质上是一样的)工作,因为它将更改链接到this.model.push({})
在。。。如果没有它,则更改周期已经完成,单击按钮时,上一个周期中的0将在新/下一个周期中更改
将setTimeout方法的持续时间设置为500
ms,您将看到它真正在做什么
ngAfterContentInit() {
this.foos.changes.pipe(delay(0)).subscribe(() => {
this.foos.forEach((foo, index) => {
setTimeout(() => {
foo.index = index;
}, 500)
});
});
}
- 它确实允许在渲染元素后设置值
DOM在避免错误的同时,您将不具有该值
在构造过程中在组件中可用,如果
你需要它
FooComponent
中的以下内容将始终导致0
和setTimeout
解决方案
ngOnInit(){
console.log(this.index)
}
将索引作为如下所示的输入传递,将使
在构造函数或FooComponent的ngOnInit
期间可用
export class FooComponent {
// index: number = 0;
@Input('index') _index:number;
您提到不希望绑定到模板中的索引,但不幸的是,在您的示例中,这是在DOM上呈现元素之前传递索引值的唯一方法,默认值为0
您可以接受FooComponent
export class FooComponent {
// index: number = 0;
@Input('index') _index:number;
然后将索引从循环传递到输入
<foo *ngFor="let foo of model; let i = index" [index]="i"></foo>
然后在视图中使用输入
selector: 'foo',
template: `<div>{{_index}}</div>`,
选择器:“foo”,
模板:`{{u index}}`,
这将允许您通过*ngFor
在app.component
级别管理索引,并在呈现时将其传递到DOM上的新元素中。。。基本上避免了将索引分配给组件变量的需要,并且还确保在渲染/类初始化时,在更改周期需要时提供true
索引
Stackblitz
我真的不知道应用程序的类型,但为了避免使用有序索引,它很有用
@Component({
selector: 'foo',
template: `<div>{{index}}</div>`,
})
export class FooComponent {
@Input() index: number = 0;
constructor(@Host() @Inject(forwardRef(()=>HelloComponent)) private hello) {}
getIndex() {
if (this.hello.foos) {
return this.hello.foos.toArray().indexOf(this);
}
return -1;
}
}
@Component({
selector: 'hello',
template: `<ng-content></ng-content>
<button (click)="addModel()">add model</button>`,
})
export class HelloComponent {
@Input() model = [];
@ContentChildren(FooComponent) foos: QueryList<FooComponent>;
constructor(private cdr: ChangeDetectorRef) {}
addModel() {
this.model.push({});
}
}