Angular - Getting out of Zone.JS for specific callbacks

倖福魔咒の 提交于 2021-02-10 14:25:43

问题


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?


回答1:


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.




回答2:


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



来源:https://stackoverflow.com/questions/58537060/angular-getting-out-of-zone-js-for-specific-callbacks

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!