【发布时间】:2017-09-13 23:11:29
【问题描述】:
在 Angular 2+ 中,自定义双向数据绑定可以通过使用 @Input 和 @Output 参数来完成。所以如果我想让一个子组件与第三方插件通信,我可以这样做:
export class TestComponent implements OnInit, OnChanges {
@Input() value: number;
@Output() valueChange = new EventEmitter<number>();
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
// Update the third party plugin with the new value from the parent component
thirdPartyPlugin.setValue(this.value);
}
}
并像这样使用它:
<test-component [(value)]="value"></test-component>
在第三方插件触发事件通知我们更改后,子组件通过调用this.valueChange.emit(newValue) 更新父组件。问题是 ngOnChanges 然后在子组件中触发,因为父组件的值已更改,这导致 thirdPartyPlugin.setValue(this.value) 被调用。但是插件已经处于正确的状态,所以这可能是不必要的/昂贵的重新渲染。
所以我经常做的是在我的子组件中创建一个标志属性:
export class TestComponent implements OnInit, OnChanges {
ignoreModelChange = false;
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
// Set ignoreModelChange to true if ngChanges will fire, so that we avoid an
// unnecessary (and potentially expensive) re-render.
if (this.value === newValue) {
return;
}
ignoreModelChange = true;
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
if (ignoreModelChange) {
ignoreModelChange = false;
return;
}
thirdPartyPlugin.setValue(this.value);
}
}
但这感觉就像一个黑客。
在 Angular 1 中,使用 = 绑定接收参数的指令有同样的问题。因此,我将通过要求 ngModelController 来完成自定义双向数据绑定,这在模型更新后不会导致重新渲染:
// Update the parent model with the new value from the third party plugin. After the model
// is updated, $render will not fire, so we don't have to worry about a re-render.
thirdPartyPlugin.onSomeEvent(function (newValue) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(newValue);
});
});
// Update the third party plugin with the new value from the parent model. This will only
// fire if the parent scope changed the model (not when we call $setViewValue).
ngModelCtrl.$render = function () {
thirdPartyPlugin.setValue(ngModelCtrl.$viewValue);
};
这行得通,但ngModelController 似乎真的是为表单元素设计的(它内置了验证等)。所以在不是表单元素的自定义指令中使用它感觉有点奇怪。
问题:Angular 2+ 中是否有在子组件中实现自定义双向数据绑定的最佳实践,在使用EventEmitter 更新父组件后不会触发子组件中的ngOnChanges?还是应该像在 Angular 1 中那样与 ngModel 集成,即使我的子组件不是表单元素?
提前致谢!
更新:我在 cmets 中查看了 @Maximus 建议的 Everything you need to know about change detection in Angular。看起来ChangeDetectorRef 上的detach 方法将阻止更新 模板 中的任何绑定,如果这是您的情况,这可能有助于提高性能。但这并不妨碍ngOnChanges 被调用:
thirdPartyPlugin.onSomeEvent(newValue => {
// ngOnChanges will still fire after calling emit
this.changeDetectorRef.detach();
this.valueChange.emit(newValue);
});
到目前为止,我还没有找到使用 Angular 的更改检测来完成此任务的方法(但我在此过程中学到了很多东西!)。
我最终尝试使用 ngModel 和 ControlValueAccessor。这似乎完成了我所需要的,因为它在 Angular 1 中表现为ngModelController:
export class TestComponentUsingNgModel implements ControlValueAccessor, OnInit {
value: number;
// Angular will pass us this function to invoke when we change the model
onChange = (fn: any) => { };
ngOnInit() {
thirdPartyPlugin.onSomeEvent(newValue => {
this.value = newValue;
// Tell Angular to update the parent component with the new value from the third
// party plugin
this.onChange(newValue);
});
}
// Update the third party plugin with the new value from the parent component. This
// will only fire if the parent component changed the model (not when we call
// this.onChange).
writeValue(newValue: number) {
this.value = newValue;
thirdPartyPlugin.setValue(this.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
}
并像这样使用它:
<test-component-using-ng-model [(ngModel)]="value"></test-component-using-ng-model>
但同样,如果自定义组件不是表单元素,使用ngModel 似乎有点奇怪。
【问题讨论】:
标签: javascript angular typescript