7

Is there a way to completely disable Angular's change detector if events that normally cause the change detection to run (setTimeout, setInterval, browser events, ajax calls etc..), are coming from a specific class (service or component)?

Namely, it seemed totally wrong to me when I found out that the setInterval registered in my service causes the global change detection to run every second..

I'm aware I can wrap my code inside the NgZone.runOutsideAngular method's callback, but I'd prefer the solution where the change detector can be disabled for an entire class, as I have other chunks of code within the service that are needlessly running the detection too.

Thanks for your help.

seidme
  • 12,543
  • 5
  • 36
  • 40
  • Have you checked: https://stackoverflow.com/questions/43108155/angular-2-how-to-keep-event-from-triggering-digest-loop ? – eko Jun 07 '17 at 07:58
  • 'Error: No provider for ChangeDetectorRef!'. Seems that provider for ChangeDetectorRef is not available for a service. – seidme Jun 07 '17 at 08:13
  • Why would you want to run change detection in a service? It does not have view. – eko Jun 07 '17 at 08:14
  • I'm not trying to run change detection in a service, I'm trying to prevent the service from running the change detection. – seidme Jun 07 '17 at 08:17
  • Can you give an example on how a service method triggers change detection? – eko Jun 07 '17 at 08:18
  • Just register the setInterval or setTimeout in any service, it automatically triggers the change detection for entire app. – seidme Jun 07 '17 at 08:22

3 Answers3

8

One possible solution might be the following @RunOutsideAngular decorator for your service:

declare let Zone: any;

export function RunOutsideAngular(target: any) {
  Object.getOwnPropertyNames(target.prototype)
    .filter(p => typeof target.prototype[p] === 'function')
    .forEach(p => {
      let originalMethod = target.prototype[p];  
      target.prototype[p] = function (...args) {
        let self = this;
        Zone.root.run(() => originalMethod.apply(self, args));
      }
    });

  let ctor: any = function (...args) {
    let self = this;
    return Zone.root.run(() => target.apply(self, args));
  };
  ctor.prototype = target.prototype;
  return ctor;
}

Plunker Example

If you want to disable only setTimeout and setInterval within some class you can patch these functions

function patchTimers(timers: any[]) {
    timers.forEach((timer) => {
        let originalMethod = window[timer];
        window[timer] = function (...args) {
            let self = this;
            if (Zone.current['__runOutsideAngular__'] === true && Zone.current.name === 'angular') {
                Zone.root.run(() => originalMethod.apply(self, args));
            } else {
                originalMethod.apply(this, arguments);
            }
        };
    })
}
patchTimers(['setTimeout', 'setInterval']);

and create decorator like this

export function RunOutsideAngular(target: any) {
    Object.getOwnPropertyNames(target.prototype)
        .filter(p => typeof target.prototype[p] === 'function')
        .forEach(p => {
            let originalMethod = target.prototype[p];
            target.prototype[p] = function (...args) {
                Zone.current['__runOutsideAngular__'] = true;
                originalMethod.apply(this, args);
                delete Zone.current['__runOutsideAngular__'];
            }
        });

    let ctor: any = function (...args) {
        Zone.current['__runOutsideAngular__'] = true;
        let instance = target.apply(this, args);
        delete Zone.current['__runOutsideAngular__'];
        return instance;
    };
    ctor.prototype = target.prototype;
    return ctor;
}

Then you can use it as follows

@RunOutsideAngular
export class Service {
  constructor() {
    setInterval(() => {
      console.log('ctor tick');
    }, 1000);
  }

  run() {
    setTimeout(() => {
      console.log('tick');
    }, 1000);

    setInterval(() => {
      console.log('tick interval');
    }, 1000)
  }
}

Plunker Example

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • This came in really handy when calling external legacy JS code from an angular project - `Zone.root.run(() => legacyCodeThatShouldReallyBePorted(foo, bar));`. no more change detection spam by `setInterval()` somewhere deep in a mess of old code. perfect! – jemand771 Dec 19 '22 at 16:58
5

You can change the detection strategy of individual components with the following:

import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'ws-layout-page',
  templateUrl: './layout-page.component.html',
  styleUrls: ['./layout-page.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class LayoutPageComponent {

}

I dont know any other method that could archieve some selective turn on/of depending of where is the information coming from

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
  • 1
    It will only stop checking view for current component – yurzui Jun 07 '17 at 09:32
  • Would be better to be in control of which aspect to exclude. Totally removing framework component can have other complicated effects as well. Goal should be to reuse or customize framework components as much as possible – NitinSingh Apr 23 '18 at 07:15
1

Change Detection starts at the very top of your component tree and is triggered by zone.js. An async operation like setTimeout is picked up by zone.js which notifies angular that changes might have happened. Angular then runs change detection top-down on the component tree. Detaching a single class from the change detector will cut out that class (i.e. directive) from change detection but won't stop change detection from being run on the rest of your component tree. For your needs ngZone.runOutsideAngular(...) is the way to go, because it stops zone.js from notifying angular about your async operation thus entirely preventing change detection from being run.

j2L4e
  • 6,914
  • 34
  • 40