【问题标题】:Prevent ngOnChanges from firing after emitting event (Angular 2+)防止 ngOnChanges 在发出事件后触发(Angular 2+)
【发布时间】: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 的更改检测来完成此任务的方法(但我在此过程中学到了很多东西!)。

我最终尝试使用 ngModelControlValueAccessor。这似乎完成了我所需要的,因为它在 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


【解决方案1】:

也遇到了这个问题(或者至少是非常相似的问题)。

我最终使用了您在上面讨论的 hacky 方法,但稍作修改后,我使用了 setTimeout 来重置状态以防万一。

(就我个人而言,如果使用双向绑定,ngOnChanges 主要是有问题的,因此如果不使用双向绑定,setTimeout 会阻止挂起的 disableOnChanges)。

changePage(newPage: number) {
    this.page = newPage;
    updateOtherUiVars();

    this.disableOnChanges = true;
    this.pageChange.emit(this.page);
    setTimeout(() => this.disableOnChanges = false, 0);     
}

ngOnChanges(changes: any) {
    if (this.disableOnChanges) {
        this.disableOnChanges = false;
        return;
    }

    updateOtherUiVars();
}

【讨论】:

  • 似乎setTimeout 也可以防止双向数据绑定中的问题,如果this.pageChange.emit(this.page) 发出与父级已经拥有的相同页码(在这种情况下ngOnChanges 不会跑)。基本上是一种不同的方式来做我所做的事情,如果我知道ngOnChanges 不会触发,那就是返回而不是发出事件。感谢您分享您的代码。
【解决方案2】:

这正是 Angular 的意图,你应该尝试使用它而不是反对Change detection works by components detecting changes in its template bindings and propagating them down the component tree. 如果您可以以依赖组件输入不变性的方式设计应用程序,则可以通过设置 @Component({changeDetection:ChangeDetectionStrategy.OnPush}) 手动控制它,这将测试引用以确定是否继续对子组件进行更改检测。

也就是说,我的经验是,第三方插件的包装器可能无法有效地处理和适当地利用这种策略。您应该尝试使用上述知识以及良好的设计选择,例如presentation vs container components 的关注点分离,以利用检测策略来实现良好的性能。

您还可以将changes: SimpleChanges 传递给ngOnInit(changes: SimpleChanges) 并检查对象以了解有关数据流的更多信息。

【讨论】:

  • 1 - 这里的输入是一个原语,所以变化检测策略不会产生影响,对吧? 2 - 在使用ngModelController 使用$setViewValue 更新父模型后,Angular 1 没有导致重新渲染,这对我来说很有意义,因为如果 DOM 更改导致您更新父模型,那么您不应该重新渲染-渲染 DOM。如果 ngModel 在 Angular 2 中的行为方式相同(尚未检查),那么在这种特定情况下,使用它可能是使用 Angular 而不是反对它的方式。 3 - 感谢SimpleChanges 的提示!
  • 真正的问题是从父级传播的变化是不可取的吗?和第三方库没有关系吗?
  • 如果不必要的重新渲染并不昂贵,那也不是问题。就像我们说子组件包装一个文本框而不是第三方插件一样。有人在文本框中键入 -> 子组件更新父组件 -> ngOnChanges 在子组件中触发 -> 文本框使用它已经拥有的相同文本进行更新(还不错)。但如果不是文本框而是第三方图表库,重新渲染可能会涉及昂贵的计算或动画。
  • 谢谢。很好的资源,但并没有改变我的答案。由于“视图”仍然与每个组件相关联,因此通过检查组件模板引用来进行更改检测的一般断言仍然是正确的。当然提供了更彻底的解释
猜你喜欢
  • 2018-06-21
  • 1970-01-01
  • 1970-01-01
  • 2010-10-27
  • 2016-07-28
  • 2016-12-06
  • 1970-01-01
  • 2012-04-09
  • 1970-01-01
相关资源
最近更新 更多