简体   繁体   中英

Angular - Getting out of Zone.JS for specific callbacks

First and foremost, I'm well aware of zone.runOutsideAngular(callback) , documented here .

What this method does is running the callback in a different zone than the Angular one.

I need to attach a very quick callback to document 's mousemove event (to count Idle time). I don't want to have the whole Zone.JS machinery of tasks appended to execution queues for this specific callback. I would really like to have the callbacks run in plain, unpatched browser runtime.

The event registration code I have is:

  private registerIdleCallback(callback: (idleFromSeconds: number) => void) {
    let idleFromSeconds = 0;
    const resetCounter = function() {
      idleFromSeconds = -1;
    };
    document.addEventListener('mousemove', resetCounter);
    document.addEventListener('keypress', resetCounter);
    document.addEventListener('touchstart', resetCounter);
    window.setInterval(function() {
      callback(++idleFromSeconds);
    }, 1000);
  }

The question is how can I get this code to use the unpatched document.addEventListener , accomplishing a complete separation from Zone.JS and true native performance?

Well, it turned out to be simple, but not straightforward, so I'm going to post the solution I found here.

What I want to do is saving a usable document.addEventListener in a global object before Zone.JS patches the hell out of the browser's native objects.

This must be done before the loading of polyfills, because it's in there that Zone is loaded. And, it must not be done in plain code in polyfills.ts , because the import statements are processed before any other code.

So I need a file zone-config.ts in the same folder of polyfills.ts . In the latter an extra import is needed:

import './zone-config'; // <- adding this line to the file, before...
// Zone JS is required by default for Angular itself.
import 'zone.js/dist/zone'; // Included with Angular CLI.

In zone-config.ts I do the sleight-of-hand:

(function(global) {
  // Save native handlers in the window.unpatched object.
  if (global.unpatched) return;

  if (global.Zone) {
    throw Error('Zone already running: cannot access native listeners');
  }

  global.unpatched = {
    windowAddEventListener: window.addEventListener.bind(window),
    windowRemoveEventListener: window.removeEventListener.bind(window),
    documentAddEventListener: document.addEventListener.bind(document),
    documentRemoveEventListener: document.removeEventListener.bind(document)
  };

  // Disable Zone.JS patching of WebSocket -- UNRELATED TO THIS QUESTION
  const propsArray = global.__Zone_ignore_on_properties || (global.__Zone_ignore_on_properties = []);
  propsArray.push({ target: WebSocket.prototype, ignoreProperties: ['close', 'error', 'open', 'message'] });

  // disable addEventListener
  const evtsArray = global.__zone_symbol__BLACK_LISTED_EVENTS || (global.__zone_symbol__BLACK_LISTED_EVENTS = []);
  evtsArray.push('message');
})(<any>window);

Now I have a window.unpatched object available, that allows me to opt out of Zone.JS completely, for very specific tasks, like the IdleService I was working on:

import { Injectable, NgZone } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter } from 'rxjs/operators';

/* This is the object initialized in zone-config.ts */
const unpatched: {
  windowAddEventListener: typeof window.addEventListener;
  windowRemoveEventListener: typeof window.removeEventListener;
  documentAddEventListener: typeof document.addEventListener;
  documentRemoveEventListener: typeof document.removeEventListener;
} = window['unpatched'];

/** Manages event handlers used to detect User activity on the page,
 * either via mouse or keyboard events. */
@Injectable({
  providedIn: 'root'
})
export class IdleService {
  private _idleForSecond$ = new BehaviorSubject<number>(0);

  constructor(zone: NgZone) {
    const timerCallback = (idleFromSeconds: number): void => this._idleForSecond$.next(idleFromSeconds);
    this.registerIdleCallback(timerCallback);
  }

  private registerIdleCallback(callback: (idleFromSeconds: number) => void) {
    let idleFromSeconds = 0;
    const resetCounter = () => (idleFromSeconds = -1);

    // runs entirely out of Zone.JS
    unpatched.documentAddEventListener('mousemove', resetCounter);
    unpatched.documentAddEventListener('keypress', resetCounter);
    unpatched.documentAddEventListener('touchstart', resetCounter);

    // runs in the Zone normally
    window.setInterval(() => callback(++idleFromSeconds), 1000);
  }

  /** Returns an observable that emits when the user's inactivity time
   * surpasses a certain threshold.
   *
   * @param seconds The minimum inactivity time in seconds.
   */
  byAtLeast(seconds: number): Observable<number> {
    return this._idleForSecond$.pipe(filter(idleTime => idleTime >= seconds));
  }

  /** Returns an observable that emits every second the number of seconds since the
   * ladt user's activity.
   */
  by(): Observable<number> {
    return this._idleForSecond$.asObservable();
  }
}

Now the stackTrace in the (idleFromSeconds = -1) callback is empty as desired.

Hope this can be of help to someone else: it's not complicated, but having all the bits in place was a bit of a chore.

I am not sure if I got what you want to do. I have made a little plunker that shows how to do an event callback from outside of Angular.

https://stackblitz.com/edit/angular-s3krnu

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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