Look directly at the summary of people’s words

preface

Take up the last chapter

The article project address

This article addresses

Angular version: 8.0.0-RC.4

Welcome to my angular class framework

The article lists

Read Chapter 1 of angular source code: Opening and platformBrowserDynamic

Read Chapter 2 of the Angular source code: The bootstrapModule

Read Chapter 3: Initializing a Zone

Chapter 4: Angular Modules and JIT-compiled Modules

Angular triggers a dirty check

Angularjs notifies a dirty check and updates the view by triggering $scope.$apply $scope.$digest.

From angularJS behavior, the Googlers found that all view changes come from the following behaviors:

  1. Browser events:onclick.onmouseover.onkeyup
  2. Timer:setInterval.setTimeout.setImmediate
  3. Asynchronous API:ajax.fetch.Promise.then
  4. The life cycle

Angular implements the zone technique used in Dart in JavaScript once.

What is a zone

Dart does not allow asynchronous operations to be performed by the current try/cacth code. Dart allows you to assign a zone to an execution object, providing a context for execution.

Within this execution environment, you can capture, intercept, or modify code behavior, such as all unhandled exceptions.

In terms of language, a zone is an execution context similar to JavaScript, providing an environment or process.

Each execution of an asynchronous method is treated as a Task in the zone, and on that Task basis, the zone provides the developer with a hook function to retrieve the information before and after the execution.

Zone

About the Zone:

zone.js/lib/zone.ts

const Zone: ZoneType = (function(global: any) {...class Zone implementsAmbientZone { ... }...let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null.null)}; .return global['Zone'] = Zone;
})(global);
Copy the code

Zone is a self-executing function

One is created when executedparentzoneSpecAre allnullAnd,name<root>ZoneInstance, soZoneIt’s a tree with a single root node

The execution ends by assigning the class Zone to the Zone property of the top-level variable.

ZoneDelegate agent

Learn about zone’s agent ZoneDelegate

zoneSpecforkThe configuration object passed in when a subzone is created

When a Zone initializes its proxy ZoneDelegate, it passes in the Zone instance and the parent zoneSpec

When the Zone is initialized, a proxy ZoneDelegate is initialized synchronously:

zone.js/lib/zone.ts

class Zone implements AmbientZone {
  ...
  constructor(parent: Zone|null, zoneSpec: ZoneSpec|null) {
      // Comment: zoneSpec is the configuration object for fork
      this._parent = parent;
      this._name = zoneSpec ? zoneSpec.name || 'unnamed' : '<root>';
      this._properties = zoneSpec && zoneSpec.properties || {};
      // Note: Implement zone proxy
      this._zoneDelegate =
          new ZoneDelegate(this.this._parent && this._parent._zoneDelegate, zoneSpec); }... }Copy the code

The ZoneDelegate constructor initializes the ZoneDelegate proxy instance with the configuration and hook functions passed in when the Zone is created through the fork method:

zone.js/lib/zone.ts

class ZoneDelegate implements AmbientZoneDelegate {
  ...
  constructor(zone: Zone, parentDelegate: ZoneDelegate|null, zoneSpec: ZoneSpec|null) {...// Comment: zoneSpec is the configuration object for fork
     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! .zone); . } fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {// Comment: execute if there is an onFork hook
      return this._forkZS ? this._forkZS.onFork! (this._forkDlgt! .this.zone, targetZone, zoneSpec) :
                            newZone(targetZone, zoneSpec); }... }Copy the code

Finally, when the agent actually executes a hook, such as when zone.fork has a hook function in its configuration object, the hook function is called to execute it

Each Zone has an instance of the ZoneDelegate agent, which calls the incoming callback function for the Zone, establishes and invokes the asynchronous task in the callback function, and catches errors in the asynchronous task

Zone.__load_patch Loads asynchronous patches

Zone violently encapsulates and replaces the browser’s asynchronous API with monkey patch, which is here.

This allows Angular to notify angular of where it is running in the context of a Zone with zone.current and perform change detection (which triggers dirty checks).

This section packages the files that replace the API actions according to the platform when packaging Zonejs. Here is an example of a browser:

zone.js/lib/browser/browser.ts

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

The method for loading the timer patch is as follows:

zone.js/lib/common/timers.ts

export function patchTimer(window: any, setName: string, cancelName: string, nameSuffix: string) {
  let setNative: Function|null = null;
  let clearNative: Function|null = null; . setNative = patchMethod(window, setName, (delegate: Function) = > function(self: any, args: any[]) {... }); . }Copy the code

PatchMethod is used to replace the native API with the API encapsulated by the zone to obtain the ability to communicate with the zone and trigger the hook function:

zone.js/lib/common/utils.ts

export function patchMethod( target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any): Function|null { let proto = target; while (proto && ! proto.hasOwnProperty(name)) { proto = ObjectGetPrototypeOf(proto); } if (! proto && target[name]) { // somehow we did not find it, but we can see it. This happens on IE for Window properties. proto = target; } const delegateName = zoneSymbol(name); let delegate: Function|null = null; if (proto && ! (delegate = proto[delegateName])) { delegate = proto[delegateName] = proto[name]; // check whether proto[name] is writable // some property is readonly in safari, such as HtmlCanvasElement.prototype.toBlob const desc = proto && ObjectGetOwnPropertyDescriptor(proto, name); if (isPropertyWritable(desc)) { const patchDelegate = patchFn(delegate! , delegateName, name); proto[name] = function() { return patchDelegate(this, arguments as any); }; attachOriginToPatched(proto[name], delegate); if (shouldCopySymbolProperties) { copySymbolProperties(delegate, proto[name]); } } } return delegate; }Copy the code

Finally, a global variable is used for patches: {[key: string]: any} to store patches, which can be used to determine whether the zone has been patched.

zone.js/lib/zone.ts

const patches: {[key: string] :any} = {};

class Zone implements AmbientZone {
  // Note: This method is used to cache monkey patches
  static __load_patch(name: string, fn: _PatchFn): void {
      if (patches.hasOwnProperty(name)) {
        if (checkDuplicate) {
          throw Error('Already loaded patch: '+ name); }}else if(! global['__Zone_disable_' + name]) {
        const perfName = 'Zone:'+ name; mark(perfName); patches[name] = fn(global, Zone, _api); performanceMeasure(perfName, perfName); }}}Copy the code

Task Asynchronous Task

In a zone, each asynchrony is called a Task: Task

type TaskType = 'microTask'|'macroTask'|'eventTask';
type TaskState = 'notScheduled'|'scheduling'|'scheduled'|'running'|'canceling'|'unknown';
interface Task {
  type: TaskType;
  state: TaskState;
  source: string;
  invoke: Function;
  callback: Function; data? : TaskData; scheduleFn? :(task: Task) = > void; cancelFn? :(task: Task) = > void;
  readonly zone: Zone;
  runCount: number;
  cancelScheduleRequest(): void;
}
Copy the code

Tasks fall into three categories:

  1. MicroTask: executed after the end of the current task and before the start of the next task. It cannot be cancelled, for examplePromise, MutationObserver, process.nexttick
  2. MacroTask: A task that is executed after a period of time and can be cancelled, for examplesetTimeout, setInterval, setImmediate, I/O, UI rendering
  3. EventTask: Listens to events, which may be executed 0 or more times. The execution time is uncertain

There are only three, so dom0-level events like img.onload=()=>{} cannot trigger dirty checks in Angular.

The state of the Task has a ‘notScheduled’ | ‘scheduling’ | ‘scheduled’ | ‘running’ | ‘canceling’ | ‘unknown’.

Setting up the hook for the execution run requires setting the configuration at zone.fork, see here:

zone.js/lib/zone.ts

interface ZoneSpec {
  ...
  /** * Allows interception of task scheduling. */onScheduleTask? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) = >Task; onInvokeTask? : (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, applyThis:any, applyArgs? :any[]) = >any;

  /** * Allows interception of task cancellation. */onCancelTask? :(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) = > any;

  /** * Notifies of changes to the task queue empty status. */onHasTask? : (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, hasTaskState: HasTaskState) =>void;
}
Copy the code
  1. onScheduleTaskCreating an Asynchronous Task
  2. onInvokeTaskExecuting asynchronous Tasks
  3. onCancelTaskCanceling an asynchronous Task
  4. onHasTaskNotifies the task queue of a change in the empty state

By setting these hooks, Angular knows which step some asynchronous tasks are executing, and it can trigger a dirty check through the hooks

Instantiation ngZone

Angular starts Zonejs in the bootstrapModule phase described above:

angular/packages/core/src/application_ref.ts

@Injectable(a)export classPlatformRef { ... bootstrapModuleFactory<M>(moduleFactory: NgModuleFactory<M>, options? : BootstrapOptions):Promise<NgModuleRef<M>> {
    // Note: We need to create the NgZone _before_ we instantiate the module,
    // as instantiating the module creates some providers eagerly.
    // So we create a mini parent injector that just contains the new NgZone and
    // pass that as parent to the NgModuleFactory.
    const ngZoneOption = options ? options.ngZone : undefined;
    const ngZone = getNgZone(ngZoneOption);
    const providers: StaticProvider[] = [{provide: NgZone, useValue: ngZone}];
    // Attention: Don't use ApplicationRef.run here,
    // as we want to be sure that all possible constructor calls are inside `ngZone.run`!
    // Comment: will be executed by onInvoke
    return ngZone.run((a)= >{... }); }... }Copy the code

Before instantiating the module factory, an NgZone instance was obtained via getNgZone:

angular/packages/core/src/application_ref.ts

function getNgZone(ngZoneOption? : NgZone | 'zone.js' | 'noop'): NgZone { let ngZone: NgZone; if (ngZoneOption === 'noop') { ngZone = new NoopNgZone(); } else { ngZone = (ngZoneOption === 'zone.js' ? undefined : ngZoneOption) || new NgZone({enableLongStackTrace: isDevMode()}); } return ngZone; }Copy the code

ngZone

angular/packages/core/src/zone/ng_zone.ts

export class NgZone {
  ...

  constructor({enableLongStackTrace = false}) {
    if (typeof Zone == 'undefined') {
      throw new Error(`In this configuration Angular requires Zone.js`);
    }

    Zone.assertZonePatched();
    const self = this as any as NgZonePrivate;
    self._nesting = 0;

    self._outer = self._inner = Zone.current;

    if ((Zone as any) ['wtfZoneSpec']) {
      self._inner = self._inner.fork((Zone as any) ['wtfZoneSpec']);
    }

    if ((Zone as any) ['TaskTrackingZoneSpec']) {
      self._inner = self._inner.fork(new ((Zone as any) ['TaskTrackingZoneSpec'] as any));
    }

    if (enableLongStackTrace && (Zone as any) ['longStackTraceZoneSpec']) {
      self._inner = self._inner.fork((Zone as any) ['longStackTraceZoneSpec']); } forkInnerZoneWithAngularBehavior(self); }... run<T>(fn: (. args:any[]) => T, applyThis? :any, applyArgs? :any[]) :T {
    return (this as any as NgZonePrivate). _inner.run(fn, applyThis, applyArgs) as T; }}Copy the code
  1. When ngZone is instantiated, a static method of the zone is first calledassertZonePatchedCheck whether the zone has been patched (whether the native API has been replaced)

zone.js/lib/zone.ts

class Zone implements AmbientZone {
 static __symbol__: (name: string) = > string = __symbol__;

 static assertZonePatched() {
   if (global['Promise'] !== patches['ZoneAwarePromise']) {
     throw new Error(
         'Zone.js has detected that ZoneAwarePromise `(window|global).Promise` ' +
         'has been overwritten.\n' +
         'Most likely cause is that a Promise polyfill has been loaded ' +
         'after Zone.js (Polyfilling Promise api is not necessary when zone.js is loaded. ' +
         'If you must load one, do so before loading zone.js.)'); }}}Copy the code
  1. Initialize the zone

Nesting Number of stack layers to execute for Zone (this comes later)

angular/packages/core/src/zone/ng_zone.ts

class NgZone {
    constructor({enableLongStackTrace = false}) {
    if (typeof Zone == 'undefined') {
      throw new Error(`In this configuration Angular requires Zone.js`);
    }

    // Note: Check whether the zone patch has been installed
    Zone.assertZonePatched();
    const self = this as any as NgZonePrivate;
    self._nesting = 0;
    // Note: This is the root zoneself._outer = self._inner = Zone.current; . }}Copy the code

_outer and _inner are the current global zone zone.current,

zone.js/lib/zone.ts

interface ZoneType {
  /** * @returns {Zone} Returns the current [Zone]. The only way to change * the current zone is by invoking a run() method, which will update the current zone for the * duration of the run method callback. */
  current: Zone;
}
Copy the code

Zone.current is a static property on a Zone that holds the global current Zone. It can only be changed by zone.run

  1. callforkInnerZoneWithAngularBehaviorFrom the current zone (This is actually the root<root>ZoneFork out an Angular zone and set the hooks

angular/packages/core/src/zone/ng_zone.ts

// Note: zone is' zone. current ', which is the root zone
function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
  zone._inner = zone._inner.fork({
    name: 'angular',
    properties: <any> {'isAngularZone': true},
    onInvokeTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task, applyThis: any,
                   applyArgs: any) :any= > {
      try {
        onEnter(zone);
        return delegate.invokeTask(target, task, applyThis, applyArgs);
      } finally{ onLeave(zone); }},// Start ngZone's run
    onInvoke: (delegate: ZoneDelegate, current: Zone, target: Zone, callback: Function,
               applyThis: any, applyArgs: any[], source: string) :any= > {
      try {
        onEnter(zone);
        return delegate.invoke(target, callback, applyThis, applyArgs, source);
      } finally {
        onLeave(zone);
      }
    },

    onHasTask:
        (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) = > {
          delegate.hasTask(target, hasTaskState);
          if (current === target) {
            // We are only interested in hasTask events which originate from our zone
            // (A child hasTask event is not interesting to us)
            if (hasTaskState.change == 'microTask') {
              zone.hasPendingMicrotasks = hasTaskState.microTask;
              checkStable(zone);
            } else if (hasTaskState.change == 'macroTask') {
              zone.hasPendingMacrotasks = hasTaskState.macroTask;
            }
          }
        },

    onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any) :boolean= > {
      delegate.handleError(target, error);
      zone.runOutsideAngular((a)= > zone.onError.emit(error));
      return false; }}); }Copy the code

Zone. fork Creates a sub-zone

Above, getNgZone will be new NgZone,

And at the end of the NgZone constructor, performed in forkInnerZoneWithAngularBehavior zone. _inner. Fork:

angular/packages/core/src/zone/ng_zone.ts

export class NgZone {
  constructor({enableLongStackTrace = false}) {... forkInnerZoneWithAngularBehavior(self); }}function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
  zone._inner = zone._inner.fork({
    name: 'angular'. }); }Copy the code

Zone.fork creates a sub-zone instance, while the fork method calls the fork method of the ZoneDelegate instance instantiated in the constructor:

zone.js/lib/zone.ts

class Zone implements AmbientZone {
  constructor(parent: Zone|null, zoneSpec: ZoneSpec|null) {
      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);
  }

  // Note: zoneSpec is the stack of configurations forked out by sub-zones
  public fork(zoneSpec: ZoneSpec): AmbientZone {
    if(! zoneSpec)throw new Error('ZoneSpec required! ');
    return this._zoneDelegate.fork(this, zoneSpec); }}Copy the code

Each Zone has an instance of the ZoneDelegate agent, which calls the incoming callback function for the Zone, establishes and invokes the asynchronous task in the callback function, and catches errors in the asynchronous task

Here we create a Zone from the root Zone by calling the fork method of the ZoneDelegate instance:

zone.js/lib/zone.ts

class ZoneDelegate implements AmbientZoneDelegate {
  ...
  fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
      // Comment: execute if there is an onFork hook
      return this._forkZS ? this._forkZS.onFork! (this._forkDlgt! .this.zone, targetZone, zoneSpec) :
                            newZone(targetZone, zoneSpec); }... }Copy the code

So, when initializing the ngZone, the zone._inner is zone. current, let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)}; When the new Zone(NULL, NULL) root Zone is created.

So zone._inner is zone. current is also

zone

So the Angular zone is a child zone from

zone fork.

Ngzone. run implements angular zones

Angular calls ngzone.run when the Zone and ZoneDelegate are initialized

angular/packages/core/src/zone/ng_zone.ts

export class NgZone {
  run<T>(fn: (. args:any[]) => T, applyThis? :any, applyArgs? :any[]) :T {
    return (this as any as NgZonePrivate). _inner.run(fn, applyThis, applyArgs) as T; }}Copy the code

Ngzone. run calls zone.run again

zone.js/lib/zone.ts

interface _ZoneFrame {
  parent: _ZoneFrame|null;
  zone: Zone;
}

let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null.null)};

class Zone implements AmbientZone {
   ...
   // Comment: Execute a function with this._zonedelegate. invoke
   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{_currentZoneFrame = {parent: _currentZoneFrame.zone: this};
     try {
       return this. _zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
     } finally{_currentZoneFrame = _currentZoneFrame.parent! ; }}... }Copy the code

_currentZoneFrame is a global object that holds the zone frame chain in the current system

On initialization, a parent: NULL, zone: new zone root _currentZoneFrame is created, so that’s where the root zone is created

It has two properties:

  1. parentPoints to the parentzoneFrame
  2. zonePoints to the currently active zone object

So _currentZoneFrame is not fixed.

Ngzone.run triggers this._zonedelegate. invoke

Zonedelegate. invoke executes the method

The zone executes a method via this._zonedelegate. invoke:

angular/packages/core/src/zone/ng_zone.ts

class ZoneDelegate implements AmbientZoneDelegate {
  private _invokeZS: ZoneSpec|null; .constructor(zone: Zone, parentDelegate: ZoneDelegate|null, zoneSpec: ZoneSpec|null) {
    this._invokeZS = zoneSpec && (zoneSpec.onInvoke ? zoneSpec : parentDelegate!._invokeZS);
  }
  ...
  invoke(
    targetZone: Zone, callback: Function, applyThis: any, applyArgs? :any[], source? :string) :any {
    return this._invokeZS ? this._invokeZS.onInvoke!
                           (this._invokeDlgt! .this._invokeCurrZone!, targetZone, callback,
                            applyThis, applyArgs, source) :
                           callback.apply(applyThis, applyArgs);
  }
  ...
}
Copy the code

The invoke method takes four arguments:

  1. targetZone: ZoneThe current callZoneDelegateZoneThe instance
  2. callback: Function The callback function is essentially justzone.run(callback)The function passed in instantiates the module component
  3. applyThis: anyBoundthis
  4. applyArgs? : any[]Arguments to the callback function
  5. source? : stringResources don’t know what to do for a while

The invoke method does this: if this._invokeZs exists and there is an onInvoke hook, execute the callback with this._invokeZs. OnInvoke, otherwise just invoke the callback function.

So back to the end of the first instantiation ngZone forkInnerZoneWithAngularBehavior code:

angular/packages/core/src/zone/ng_zone.ts

function forkInnerZoneWithAngularBehavior(zone: NgZonePrivate) {
  zone._inner = zone._inner.fork({
    name: 'angular'. onInvoke: (delegate: ZoneDelegate, current: Zone, target: Zone, callback:Function,
               applyThis: any, applyArgs: any[], source: string) :any= > {
      try {
        onEnter(zone);
        return delegate.invoke(target, callback, applyThis, applyArgs, source);
      } finally{ onLeave(zone); }},... }); }Copy the code

So onEnter(zone) is triggered when the onInvoke hook invokes the run callback; onLeave(zone); :

angular/packages/core/src/zone/ng_zone.ts

function onEnter(zone: NgZonePrivate) {
  zone._nesting++;
  if (zone.isStable) {
    zone.isStable = false;
    zone.onUnstable.emit(null); }}function onLeave(zone: NgZonePrivate) {
  zone._nesting--;
  checkStable(zone);
}
Copy the code

When entering the execution stack, ngzone. _nesting ++ leaves —

The onInvoke hook function also calls delegate: ZoneDelegate is the Parent of angular Zone < zone >zone’s invoke method:

zone.js/lib/zone.ts

class ZoneDelegate implements AmbientZoneDelegate {
  ...
  invoke(
        targetZone: Zone, callback: Function, applyThis: any, applyArgs? :any[], source? :string) :any {
      return this._invokeZS ? this._invokeZS.onInvoke!
                              (this._invokeDlgt! .this._invokeCurrZone!, targetZone, callback,
                               applyThis, applyArgs, source) :
                              callback.apply(applyThis, applyArgs);
    }
    ...
}
Copy the code

But because the

Zone does not have ZoneDelegate, callback.apply(applyThis, applyArgs) is just done;

So why do we have onInvoke recursively call delegate.invoke?

The Zone instance is actually a tree structure

My guess is that Angular wants the proxy object that triggers each parent Zone to pass through and trigger the corresponding hook function to invoke the corresponding action, leaving the root proxy object to execute the actual callback function.

So to summarize a little bit:

  1. ngZone.runCall the angular zone you createdrunmethods
  2. zone.runThe configuration passed in when fork Angular is called againonInvokehook
  3. configurationonInvokeThe hook performs the incoming againrunThe callbackThat is:onInvokeHooks perform functions that create modules and components
  4. Similar to AOP, inonInvokeWhen the callback is executed, the cut will passonEnter(zone); onLeave(zone);To useEventEmitterNotify the presents

At this point, the zone’s environment is initialized

conclusion

The following words are summarized:

  1. When zone.js is introduced, the self-executing function zone creates a

    zone
  2. A patch to replace the native asynchronous API is implemented when zone.js is introduced
  3. bootstrapModuleFactoryIs used first when booting the root modulegetNgZone<root>Zoneaangular ZoneAnd set several hooks
  4. callngZone.runAnd pass in callback functions that instantiate module factories and components
  5. ngZone.runcallangular Zonerunmethods
  6. angular ZonerunThe method callforkangular ZoneIs in the incoming configurationonInvokeperform
  7. angular ZoneonInvokeTriggers the enter/leave section operation and calls up the agent of the parent zoneonInvokeHook function
  8. By the childZoneTo faceZoneRecursive implementationonInvokeHooks that trigger the corresponding section function
  9. Finally, the module factory and component callback functions are instantiated by the

    Zone