Learn the essence
The Zone.js project provides multiple asynchronous execution contexts running within a single JavaScript thread (i.e. within the main browser UI thread or within a single web worker). Zones are a way of sub-dividing a single thread using the JavaScript event loop. A single zone does not cross thread boundaries. A nice way to think about zones is that they sub-divide the stack within a JavaScript thread into multiple mini- stacks, and sub-divide the JavaScript event loop into multiple mini event loops that seemingly run concurrently (but really share the same VM event loop). In effect, Zone.js helps you write multithreaded code within a single thread.
What is Zone
A JavaScript VM is embedded in a host environment, such as a browser or Node, which is responsible for scheduling JavaScript execution through tasks. A Zone is an execution context that persists across async tasks, and allows the creator of the zone to observe and control execution of the code within the zone.
A zone is responsible for:
- Persisting the zone across async task execution.
- Exposing visibility into the task scheduling and processing of host environment (Scoped to the current zone)
Why use Zone
- Zones are useful for debugging, testing, profiling.
- Zones are useful for frameworks to know when to render.
- Zones are useful for tracking resources which persist across async operations and can automatically release/cleanup the resources.
- Zones are composable
Types of Tasks
MicroTask
: A microtask is work which will execute as soon as possible on empty stack frame. A microtask is guaranteed to run before host environment performs rendering or I/O operations. A microtask queue must be empty before another MacroTask or EventTask runs. (i.e. Promise.then() executes in microtask)MacroTask
: Macro tasks are interleaved with rendering and I/O operations of the host environment. (ie setTimeout, setInterval, etc..) Macro tasks are guaranteed to run at least once or canceled (some can run repeatedly such as setInterval). Macro tasks have an implied execution order.EventTask
: Event tasks are similar to macro tasks, but unlike macro tasks they may never run. When an EventTask is run, it pre-empts whatever the next task is the macro task queue. Event tasks do not create a queue. (i.e. user click, mousemove, XHR state change.)
Type | Scheduled | Execution |
---|---|---|
MicroTask | Microtasks are scheduled by promises whenever they need to invoke the thenCallback. promise.then(thenCallback). |
The execution of the thenCallback runs inside a microtask. Once the microtask is scheduled it can not be canceled and it is guaranteed to run exactly once. |
MacroTask | Macro tasks are scheduled by user code using explicit API such as setTimeout(callback), setInterval(callback), etc.. | The execution of the callback runs inside a macrotask after any rendering and I/O operations has completed. Once a macro task is completed the microtask queue is drained before the execution is handed to the host environment for more rendering and I/O operations. |
EventTask | Event tasks are scheduled using addEventListener(‘click’, eventCallback), or similar mechanisms. | The execution of the event task may never happen, come at an unpredictable time, and can occur more than once, therefore there is no way to know how many times it will be executed. |
lib
https://www.npmjs.com/package/zone.js
How to use
- Entering Zones, Forking and Stack FramesEntering Zones, Forking and Stack Frames
- Zone 有层级关系,ChildZone 是知晓其 ParentZone 的。
- It is important to understand that a given stack frame can only be associated with one zone.
- Zones monkey patch methods only once.
常用场景:
- Log long error stack trace
- Profiling async task timing profiling
内置场景:
How to implement
Basic Structure
而在核心源码和职能层 👇
- ZoneSpec:自定义 Zone 对核心功能的实现(类似于一个新的 Strategy),可以定义当下 Zone 范围内的 Task 的执行、调度、错误检测方式。
/**
* Provides a way to configure the interception of zone events.
*
* Only the `name` property is required (all other are optional).
*/
interface ZoneSpec {
/**
* The name of the zone. Useful when debugging Zones.
*/
name: string;
/**
* A set of properties to be associated with Zone. Use [Zone.get] to retrieve them.
*/
properties?: {[key: string]: any};
/**
* Allows the interception of zone forking.
*
* When the zone is being forked, the request is forwarded to this method for interception.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param zoneSpec The argument passed into the `fork` method.
*/
onFork?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
zoneSpec: ZoneSpec) => Zone;
/**
* Allows interception of the wrapping of the callback.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param delegate The argument passed into the `wrap` method.
* @param source The argument passed into the `wrap` method.
*/
onIntercept?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
source: string) => Function;
/**
* Allows interception of the callback invocation.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param delegate The argument passed into the `run` method.
* @param applyThis The argument passed into the `run` method.
* @param applyArgs The argument passed into the `run` method.
* @param source The argument passed into the `run` method.
*/
onInvoke?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,
applyThis: any, applyArgs?: any[], source?: string) => any;
/**
* Allows interception of the error handling.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param error The argument passed into the `handleError` method.
*/
onHandleError?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
error: any) => boolean;
/**
* Allows interception of task scheduling.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param task The argument passed into the `scheduleTask` method.
*/
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.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param task The argument passed into the `cancelTask` method.
*/
onCancelTask?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => any;
/**
* Notifies of changes to the task queue empty status.
*
* @param parentZoneDelegate Delegate which performs the parent [ZoneSpec] operation.
* @param currentZone The current [Zone] where the current interceptor has been declared.
* @param targetZone The [Zone] which originally received the request.
* @param hasTaskState
*/
onHasTask?:
(parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,
hasTaskState: HasTaskState) => void;
}
- ZoneDelegate:
/**
* A delegate when intercepting zone operations.
*
* A ZoneDelegate is needed because a child zone can't simply invoke a method on a parent zone. For
* example a child zone wrap can't just call parent zone wrap. Doing so would create a callback
* which is bound to the parent zone. What we are interested in is intercepting the callback before
* it is bound to any zone. Furthermore, we also need to pass the targetZone (zone which received
* the original request) to the delegate.
*
* The ZoneDelegate methods mirror those of Zone with an addition of extra targetZone argument in
* the method signature. (The original Zone which received the request.) Some methods are renamed
* to prevent confusion, because they have slightly different semantics and arguments.
*
* - `wrap` => `intercept`: The `wrap` method delegates to `intercept`. The `wrap` method returns
* a callback which will run in a given zone, where as intercept allows wrapping the callback
* so that additional code can be run before and after, but does not associate the callback
* with the zone.
* - `run` => `invoke`: The `run` method delegates to `invoke` to perform the actual execution of
* the callback. The `run` method switches to new zone; saves and restores the `Zone.current`;
* and optionally performs error handling. The invoke is not responsible for error handling,
* or zone management.
*
* Not every method is usually overwritten in the child zone, for this reason the ZoneDelegate
* stores the closest zone which overwrites this behavior along with the closest ZoneSpec.
*
* NOTE: We have tried to make this API analogous to Event bubbling with target and current
* properties.
*
* Note: The ZoneDelegate treats ZoneSpec as class. This allows the ZoneSpec to use its `this` to
* store internal state.
*/
interface ZoneDelegate {
zone: Zone;
fork(targetZone: Zone, zoneSpec: ZoneSpec): Zone;
intercept(targetZone: Zone, callback: Function, source: string): Function;
invoke(targetZone: Zone, callback: Function, applyThis?: any, applyArgs?: any[], source?: string):
any;
handleError(targetZone: Zone, error: any): boolean;
scheduleTask(targetZone: Zone, task: Task): Task;
invokeTask(targetZone: Zone, task: Task, applyThis?: any, applyArgs?: any[]): any;
cancelTask(targetZone: Zone, task: Task): any;
hasTask(targetZone: Zone, isEmpty: HasTaskState): void;
}
Task
/** * Represents work which is executed with a clean stack. * * Tasks are used in Zones to mark work which is performed on clean stack frame. There are three * kinds of task. [MicroTask], [MacroTask], and [EventTask]. * * A JS VM can be modeled as a [MicroTask] queue, [MacroTask] queue, and [EventTask] set. * * - [MicroTask] queue represents a set of tasks which are executing right after the current stack * frame becomes clean and before a VM yield. All [MicroTask]s execute in order of insertion * before VM yield and the next [MacroTask] is executed. * - [MacroTask] queue represents a set of tasks which are executed one at a time after each VM * yield. The queue is ordered by time, and insertions can happen in any location. * - [EventTask] is a set of tasks which can at any time be inserted to the end of the [MacroTask] * queue. This happens when the event fires. * */ interface Task { /** * Task type: `microTask`, `macroTask`, `eventTask`. */ type: TaskType; /** * Task state: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`, `unknown`. */ state: TaskState; /** * Debug string representing the API which requested the scheduling of the task. */ source: string; /** * The Function to be used by the VM upon entering the [Task]. This function will delegate to * [Zone.runTask] and delegate to `callback`. */ invoke: Function; /** * Function which needs to be executed by the Task after the [Zone.currentTask] has been set to * the current task. */ callback: Function; /** * Task specific options associated with the current task. This is passed to the `scheduleFn`. */ data?: TaskData; /** * Represents the default work which needs to be done to schedule the Task by the VM. * * A zone may choose to intercept this function and perform its own scheduling. */ scheduleFn?: (task: Task) => void; /** * Represents the default work which needs to be done to un-schedule the Task from the VM. Not all * Tasks are cancelable, and therefore this method is optional. * * A zone may chose to intercept this function and perform its own un-scheduling. */ cancelFn?: (task: Task) => void; /** * @type {Zone} The zone which will be used to invoke the `callback`. The Zone is captured * at the time of Task creation. */ readonly zone: Zone; /** * Number of times the task has been executed, or -1 if canceled. */ runCount: number; /** * Cancel the scheduling request. This method can be called from `ZoneSpec.onScheduleTask` to * cancel the current scheduling interception. Once canceled the task can be discarded or * rescheduled using `Zone.scheduleTask` on a different zone. */ cancelScheduleRequest(): void; }
Core Theory
基础原理:「原生对象劫持」Monkey Patch:
如下:
// Save the original reference to setTimeout
let originalSetTimeout = window.setTimeout;
// Overwrite the API with a function which wraps callback in zone.
window.setTimeout = function(callback, delay) {
// Invoke the original API but wrap the callback in zone.
return originalSetTimeout(
// Wrap the callback method
Zone.current.wrap(callback),
delay
);
}
// Return a wrapped version of the callback which restores zone.
Zone.prototype.wrap = function(callback) {
// Capture the current zone
let capturedZone = this;
// Return a closure which executes the original closure in zone.
return function() {
// Invoke the original callback in the captured zone.
return capturedZone.runGuarded(callback, this, arguments);
};
};
再比如,如果将 SetTimeout 函数调度到 Zone.js 的任务队列:
// Save the original reference to setTimeout
let originalSetTimeout = window.setTimeout;
// Overwrite the API with a function which wraps callback in zone.
window.setTimeout = function(callback, delay) {
// Use scheduleTask API on the current zone.
Zone.current.scheduleMacroTask(
// Debug information
'setTimeout',
// callback which needs to execute in the current zone.
callback,
// optional data such as if task is recurring.
null,
// Default schedule behavior
(task) => {
return originalSetTimeout(
// Use the task invoke method, so that the task can
// call callback in the correct zone.
task.invoke,
// original delay information
delay
);
});
}