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 Error
Cannot 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 itparent
Set to the region for the branchzone.run(callback, ...)
: Synchronously calls a function in a given regionzone.runGuarded(callback, ...)
And:run
Runtime 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.bind
It 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 inasync
The operation is called beforescheduled
, which meansasync
The action is about to be sent to the browser (or NodeJS) to be scheduled for later runtimeonInvokeTask
: This callback will be called before the asynchronous callback is actually calledonHasTask
: When the status of the task queue isempty
This 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 Error
Cannot locate the context exactly: use lifecycle hooksonHandleError
Processing 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