简体   繁体   English

发射事件后防止ngOnChanges触发(Angular 2+)

[英]Prevent ngOnChanges from firing after emitting event (Angular 2+)

In Angular 2+, custom two-way databinding can be accomplished by using @Input and @Output parameters. 在Angular 2+中,可以使用@Input@Output参数完成自定义双向数据绑定。 So if I want a child component to communicate with a third party plugin, I could do it as follows: 因此,如果我想让子组件与第三方插件进行通信,我可以按如下方式进行:

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);
    }
}

And use it like this: 并像这样使用它:

<test-component [(value)]="value"></test-component>

After the third party plugin fires an event to notify us of a change, the child component updates the parent component by calling this.valueChange.emit(newValue) . 在第三方插件触发事件以通知我们更改后,子组件通过调用this.valueChange.emit(newValue)更新父组件。 The issue is that ngOnChanges then fires in the child component because the parent component's value has changed, which causes thirdPartyPlugin.setValue(this.value) to be called. 问题是ngOnChanges随后在子组件中触发,因为父组件的值已更改,这会导致调用thirdPartyPlugin.setValue(this.value) But the plugin is already in the correct state, so this is a potentially unnecessary/expensive re-render. 但是插件已经处于正确状态,因此这可能是不必要的/昂贵的重新渲染。

So what I often do is create a flag property in my child component: 所以我经常做的是在我的子组件中创建一个标志属性:

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);
    }
}

But this feels like a hack. 但这感觉就像一个黑客。

In Angular 1, directives which took in a parameter using the = binding had the same exact issue. 在Angular 1中,使用= binding接受参数的指令具有相同的确切问题。 So instead, I would accomplish custom two-way databinding by requiring ngModelController , which did not cause a re-render after a model update: 因此,我会通过要求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);
};

This worked, but ngModelController really seems to be designed for form elements (it has built in validation, etc.). 这工作,但ngModelController似乎真的是为表单元素设计的(它内置了验证等)。 So it felt a bit odd to use it in custom directives which are not form elements. 因此,在不是表单元素的自定义指令中使用它感觉有点奇怪。

Question: Is there a best practice in Angular 2+ for implementing custom two-way databinding in a child component, which does not trigger ngOnChanges in the child component after updating the parent component using EventEmitter ? 问:是否有角最佳实践2+在一个子组件,这不会触发实现自定义双向数据绑定ngOnChanges更新使用父组件后,子组件EventEmitter Or should I integrate with ngModel just as I did in Angular 1, even if my child component is not a form element? 或者我应该像在Angular 1中那样与ngModel集成,即使我的子组件不是表单元素?

Thanks in advance! 提前致谢!


Update : I checked out Everything you need to know about change detection in Angular suggested by @Maximus in the comments. 更新 :我在评论中检查了@Maximus建议的Angular变更检测所需的所有内容 It looks like the detach method on ChangeDetectorRef will prevent any bindings in the template from being updated, which could help with performance if that's your situation. 看起来ChangeDetectorRef上的detach方法将阻止更新模板中的任何绑定,如果这是你的情况,这可能有助于提高性能。 But it does not prevent ngOnChanges from being called: 但它不会阻止ngOnChanges

thirdPartyPlugin.onSomeEvent(newValue => {
    // ngOnChanges will still fire after calling emit

    this.changeDetectorRef.detach();
    this.valueChange.emit(newValue);
});

So far I haven't found a way to accomplish this using Angular's change detection (but I learned a lot in the process!). 到目前为止,我还没有找到使用Angular的变化检测来实现这一目标的方法(但我在这个过程中学到了很多东西!)。

I ended up trying this with ngModel and ControlValueAccessor . 我最终用ngModelControlValueAccessor尝试了这个。 This seems to accomplish what I need since it behaves as ngModelController in Angular 1: 这似乎完成了我需要的,因为它在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;
    }
}

And use it like this: 并像这样使用它:

<test-component-using-ng-model [(ngModel)]="value"></test-component-using-ng-model>

But again, if the custom component is not a form element, using ngModel seems a bit odd. 但同样,如果自定义组件不是表单元素,使用ngModel似乎有点奇怪。

Also ran into this problem (or at least something very similar). 也遇到了这个问题(或者至少是非常相似的东西)。

I ended up using hacky approach you discussed above but with a minor modification, I used setTimeout in order to reset state just in case. 我最后使用了你上面讨论过的hacky方法,但稍作修改,我使用了setTimeout来重置状态以防万一。

(For me personally ngOnChanges was mainly problematic if using two-way binding, so the setTimeout prevents a hanging disableOnChanges if NOT using two-way binding). (对我个人来说,如果使用双向绑定,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();
}

This is exactly the intention of Angular and its something you should try to work with rather than against . 这正是Angular的意图以及你应该尝试使用而不是反对的东西 Change detection works by components detecting changes in its template bindings and propagating them down the component tree. 更改检测由检测其模板绑定中的更改并将其沿组件树向下传播的组件工作。 If you can design your application in such a way that you are relying on the immutability of components inputs', you can control this manually by setting @Component({changeDetection:ChangeDetectionStrategy.OnPush}) which will test references to determine whether to continue change detection on children components. 如果您可以依赖于组件输入的不变性来设计应用程序,则可以通过设置@Component({changeDetection:ChangeDetectionStrategy.OnPush})来手动控制它,该组件将测试引用以确定是否继续更改检测儿童成分。

So, that said, my experience is that wrappers of 3rd party plugins may not efficiently handle and take advantage of this type of strategy appropriately. 所以说,我的经验是,第三方插件的包装可能无法有效地处理和利用这种类型的策略。 You should attempt to use knowledge of the above, together with good design choices like the separation of concerns of presentation vs container components to leverage the detection strategy to achieve good performance. 您应该尝试使用上述知识,以及良好的设计选择,例如分离表示与容器组件的关注点,以利用检测策略来实现良好的性能。

You can also pass changes: SimpleChanges to ngOnInit(changes: SimpleChanges) and inspect the object to learn more about your data flow. 您还可以将changes: SimpleChanges传递给ngOnInit(changes: SimpleChanges)并检查对象以了解有关数据流的更多信息。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM