This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

As a front-end framework designed for “big front-end projects,” Angular has a lot of design to learn from, and this series focuses on how those designs and features work. This article focuses on NgZone’s core Angular capabilities, which are implemented in Zone.js, so this article introduces zone.js first.

Angular uses dirty check for data change detection, which has been criticized as a performance problem in AngularJS versions. After Angular(2+), the introduction of modular organization and NgZone design improved the performance of dirty checking.

NgZone was introduced not only to solve the problem of dirty checking, but also to solve many of the context problems of asynchronous Javascript programming, for which zone.js is a scoped solution.

zone.js

A Zone is an execution context that persists across asynchronous tasks. Zone.js provides the following capabilities:

  • Provides execution context between asynchronous operations
  • Provide asynchronous lifecycle hooks
  • Provides uniform asynchronous error handling

The confusion of asynchronous operations

In Javascript, a stack is created during code execution, and functions are executed on the stack.

For asynchronous operations, the context can change when asynchronous code and functions are executed, which can cause some difficulties. Such as:

  • When asynchronous code executes, the context changes, resulting in inconsistent expectations
  • throw ErrorCannot locate the context accurately
  • Test the execution time of a function, but because the function has asynchronous logic, you cannot get an exact execution time

In general, the context of asynchronous code execution can be solved by passing arguments or global variables, but neither approach is very elegant (especially global variables). Zone.js is proposed to solve the above problems, let’s take a look.

The design of the zone. Js

Zone.js is inspired by Dart Zones, and you can also think of it as TLS in JavaScript VMS — thread local storage.

A zone has the concept of the current region: the current region is an asynchronous context propagated with all asynchronous operations and represents the region associated with the currently executing stack frame/asynchronous task.

The current context can be obtained using zone. current, which is analogous to this in Javascript, and tracked using the _currentZoneFrame variable in zone.js. Each zone has a name attribute, which is mainly used for tools and debugging purposes. Zone.js also defines methods for manipulating zones:

  • zone.fork(zoneSpec): Creates a new subregion and places itparentSet to the region for the branch
  • zone.run(callback, ...): Synchronously calls a function in a given region
  • zone.runGuarded(callback, ...)And:runRuntime errors are caught the same and a mechanism is provided to intercept them. If any parent region does not handle an error, it is rethrown.
  • zone.wrap(callback): Generates a new function that binds the region in a closure and executeszone.runGuarded(callback)When executed, with JavaScriptFunction.prototype.bindIt works similarly.

We can see that the main implementation logic for zones (new Zone()/fork()/run())) is also relatively simple:

class Zone implements AmbientZone {
  // Get the root region
  static get root() :AmbientZone {
    let zone = Zone.current;
    // Find the outermost region, the parent region is itself
    while (zone.parent) {
      zone = zone.parent;
    }
    return zone;
  }
  // Get the current region
  static get current() :AmbientZone {
    return _currentZoneFrame.zone;
  }
  private _parent: Zone|null; / / parent area
  private _name: string; // Region name
  private _properties: {[key: string] :any};
  // Intercepting delegate for area operations for lifecycle hook related processing
  private _zoneDelegate: ZoneDelegate;

  constructor(parent: Zone|null, zoneSpec: ZoneSpec|null) {
    // When creating a region, set its properties
    this._parent = parent;
    this._name = zoneSpec ? zoneSpec.name || 'unnamed' : '<root>';
    this._properties = zoneSpec && zoneSpec.properties || {};
    this._zoneDelegate =
        new ZoneDelegate(this.this._parent && this._parent._zoneDelegate, zoneSpec);
  }
  // fork produces sub-areas
  public fork(zoneSpec: ZoneSpec): AmbientZone {
    if(! zoneSpec)throw new Error('ZoneSpec required! ');
    // Call new Zone() to generate child zones with the current Zone as the parent Zone
    return this._zoneDelegate.fork(this, zoneSpec);
  }
  // Run some code synchronously in the zone
  public run(callback: Function, applyThis? :any, applyArgs? :any[], source? :string) :any;
  public run<T>(
      callback: (. args:any[]) = >T, applyThis? :any, applyArgs? :any[], source? :string): T {
    // Ready to execute, push processing
    _currentZoneFrame = {parent: _currentZoneFrame, zone: this};
    try {
      // Use callback.apply(applyThis, applyArgs)
      return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
    } finally {
      // After execution, the stack is removed_currentZoneFrame = _currentZoneFrame.parent! ; }}... }Copy the code

In addition to the above, zones provide a number of methods to run, schedule, and cancel tasks, including:

interface Zone {
  ...
  // Execute the task by restoring zone. currentTask in the task arearunTask<T>(task: Task, applyThis? :any, applyArgs? :any): T;
  // Schedule a MicroTask
  scheduleMicroTask(source: string.callback: Function, data? : TaskData, customSchedule? :(task: Task) = > void): MicroTask;
  // Schedule a MacroTask
  scheduleMacroTask(source: string.callback: Function, data? : TaskData, customSchedule? :(task: Task) = > void, customCancel? :(task: Task) = > void): MacroTask;
  // Schedule an EventTask
  scheduleEventTask(source: string.callback: Function, data? : TaskData, customSchedule? :(task: Task) = > void, customCancel? :(task: Task) = > void): EventTask;
  // Schedule existing tasks (useful for rescheduling cancelled tasks)
  scheduleTask<T extends Task>(task: T): T;
  ZoneSpec. OnCancelTask is used to configure blocking
  cancelTask(task: Task): any;
}
Copy the code

Lets asynchronous logic run in the specified region

In zone.js, zone.fork allows you to create sub-regions, and zone.run allows functions (including asynchronous logic within functions) to run within the specified regions. Here’s an example:

const zoneBC = Zone.current.fork({name: 'BC'});
function c() {
    console.log(Zone.current.name);  // BC
}
function b() {
    console.log(Zone.current.name);  // BC
    setTimeout(c, 2000);
}
function a() {
    console.log(Zone.current.name);  // <root>
    zoneBC.run(b);
}

a();
Copy the code

The execution looks like this:

In fact, the call stack for each asynchronous task starts at the root region. Therefore, in zone.js the zone restores the correct zone using the information associated with the task, and then calls the task:

The functions and implementations of zone.fork () and zone.run () have been described above. So how does zone.js recognize asynchronous tasks? Zone.js uses monkey patches to intercept asynchronous apis, including DOM events, XMLHttpRequest, and NodeJS apis such as EventEmitter, FS, etc.

// Loads the patch for the specified local module
static __load_patch(name: string.fn: _PatchFn, ignoreDuplicate = false) :void {
  // Check whether the patch is loaded
  if (patches.hasOwnProperty(name)) {
    if(! ignoreDuplicate && checkDuplicate) {throw Error('Already loaded patch: ' + name);
    }
  // Check whether a patch needs to be loaded
  } else if (!global['__Zone_disable_' + name]) {
    const perfName = 'Zone:' + name;
    // Use performance. Mark to mark the timestamp
    mark(perfName);
    // Intercepts the specified asynchronous API and processes it
    patches[name] = fn(global, Zone, _api);
    // Use performance. Measure to calculate the timeperformanceMeasure(perfName, perfName); }}Copy the code

Using timers such as setTimeout as an example, by intercepting and capturing specific apis:

Zone.__load_patch('timers'.(global: any) = > {
  const set = 'set';
  const clear = 'clear';
  patchTimer(global, set, clear, 'Timeout');
  patchTimer(global, set, clear, 'Interval');
  patchTimer(global, set, clear, 'Immediate');
});
Copy the code

PatchTimer has done a lot of compatibility logic processing, including node.js and browser environment detection and processing, among which the key implementation logic is as follows:

// Check whether the function attribute is writable
if (isPropertyWritable(desc)) {
  constpatchDelegate = patchFn(delegate! , delegateName, name);// Modify the default behavior of functions
  proto[name] = function() {
    return patchDelegate(this.arguments as any);
  };
  attachOriginToPatched(proto[name], delegate);
  if(shouldCopySymbolProperties) { copySymbolProperties(delegate, proto[name]); }}// patchFn is used to create MacroTask with the current area
const patchFn = function(self: any, args: any[]) {
  if (typeof args[0= = ='function') {...const callback = args[0];
    args[0] = function timer(this: unknown) {
      try {
        // Execute this function
        return callback.apply(this.arguments);
      } finally {
        // Do some cleanup, such as deleting references to tasks, etc}}};/ / create MacroTask tasks using the current area, call Zone. The current. ScheduleMacroTask
    const task = scheduleMacroTaskWithCurrentZone(setName, args[0], options, scheduleTask, clearTask);
    if(! task) {return task;
    }
    // Some compatibility work, such as storing the task reference in a timerId object for clearTimeout in a NodeJS environment
    return task;
  } else {
    // When an exception occurs, the call is returned directly
    return delegate.apply(window, args); }};Copy the code

Here, the Timer related task is created and added to the Zone task for processing. In zone.js, there is a splitting of asynchronous tasks into three types:

type TaskType = 'microTask'|'macroTask'|'eventTask';
Copy the code

Zone.js supports selective patching. For more details, refer to Zone.js’s Support for Standard apis.

Life cycle of task execution

Zone.js provides asynchronous operation lifecycle hooks with which a zone can monitor and intercept all lifecycle of an asynchronous operation:

  • onScheduleTask: This callback will be executed inasyncThe operation is called beforescheduled, which meansasyncThe action is about to be sent to the browser (or NodeJS) to be scheduled for later runtime
  • onInvokeTask: This callback will be called before the asynchronous callback is actually called
  • onHasTask: When the status of the task queue isemptyThis callback is invoked when changes are made between andnot empty

The full lifecycle hooks include:

interface ZoneSpec {
  // Allow the interception of zone.fork. When the area is forked, the request will be forwarded to this method for interceptiononFork? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, zoneSpec: ZoneSpec) = > Zone;
  // Allow the wrap to intercept the callbackonIntercept? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function, source: string) = > Function;
  // Allow intercepting callback callsonInvoke? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function, applyThis: any, applyArgs? :any[], source? :string) = > any;
  // Allow intercepting error handlingonHandleError? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any) = > boolean;
  // Allow interception of mission plansonScheduleTask? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) = > Task;
  // Allow interception task callbackonInvokeTask? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, applyThis: any, applyArgs? :any[]) = > any;
  // Allow interception to be cancelledonCancelTask? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) = > any;
  // Notifies changes to the empty state of the task queueonHasTask? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, hasTaskState: HasTaskState) = > void;
}
Copy the code

These lifecycle hook callbacks are created and passed into ZoneDelegate via new zone () at zone.fork() :

class Zone implements AmbientZone {
  constructor(parent: Zone|null, zoneSpec: ZoneSpec|null){...this._zoneDelegate = new ZoneDelegate(this.this._parent && this._parent._zoneDelegate, zoneSpec); }}Copy the code

Take onFork for example:

class ZoneDelegate implements AmbientZoneDelegate {
  constructor(zone: Zone, parentDelegate: ZoneDelegate|null, zoneSpec: ZoneSpec|null){...// Manage the onFork hook callback
    this._forkZS = zoneSpec && (zoneSpec && zoneSpec.onFork ? zoneSpec : parentDelegate! ._forkZS);this._forkDlgt = zoneSpec && (zoneSpec.onFork ? parentDelegate : parentDelegate! ._forkDlgt);this._forkCurrZone =
        zoneSpec && (zoneSpec.onFork ? this.zone : parentDelegate! ._forkCurrZone); }// When the fork call is made, the onFork hook callback is checked for registration and the call is made
  fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
    return this._forkZS ? this._forkZS.onFork! (this._forkDlgt! .this.zone, targetZone, zoneSpec) : newZone(targetZone, zoneSpec); }}Copy the code

This is the implementation of the lifecycle hook in zone.js. With these hooks, we can do many other useful things, such as parsing, logging, and limiting function execution and invocation.

conclusion

This article focuses on zone.js, which is designed to solve the problem of execution context in asynchronous programming.

In zone.js, the current region is the asynchronous context propagated with all asynchronous operations, comparable to this in Javascript. Zone.fork allows you to create sub-regions, and zone.run allows functions (including asynchronous logic within functions) to run in the specified regions.

Zone.js provides a rich set of lifecycle hooks that can be used to solve the problems we mentioned earlier with zone.js’s regional capabilities and lifecycle hooks:

  • When the asynchronous code executes, the context changes, causing inconsistent expectations: the Zone is used to execute the relevant code
  • throw ErrorCannot locate the context exactly: use lifecycle hooksonHandleErrorProcessing and tracking
  • Test the execution time of a function, but because the function has asynchronous logic, you can’t get an exact execution time: use lifecycle hooks to get the exact execution time

reference

  • Deep dive into Zone.js [Part 1: Execution Context]
  • Deep dive into Zone.js [Part 2: LifeCycle Hooks]
  • I reverse-engineered Zones (zone.js) and here is what I’ve found