Zone.js internals
@JiaLiPassion
Agenda
- What's Zone.js and internal implementation
- When and why you need Zone.js
- Some features of Zone.js in Angular
What is Zone.js
A Zone is an execution context that persists across async tasks.
Created by Brian Ford inspired by Dart.
What zone.js can do
-
Provide execution context
-
Provide async task lifecycle hook
-
Provide error handler for async operations
Execution Context
const testObj = {
testFunc: function() {
console.log('this in testFunc is:', this);
}
};
testObj.testFunc();
const newTestFunc = testObj.testFunc;
newTestFunc();
const newObj = {};
newTestFunc.apply(newObj);
const bindObj = {};
const boundFunc = testObj.testFunc.bind(bindObj);
boundFunc();
Execution context in zone.js
zone.run(function() {
// function is in the zone
// just like `this`, we have a zoneThis === zone
expect(zoneThis).toBe(zone);
setTimeout(function() {
// the callback of async operation
// will also have a zoneThis === zone
// which is the zoneContext when this async operation
// is scheduled.
expect(zoneThis).toBe(zone);
});
Promise.resolve(1).then(function() {
// all async opreations will be in the same zone
// when they are scheduled.
expect(zoneThis).toBe(zone);
});
});
How to get `zoneThis`
zone.run(function() {
expect(Zone.current).toBe(zone);
setTimeout(function() {
expect(Zone.current).toBe(zone);
});
});
share context
const zone = Zone.current.fork({
name: 'zone',
properties: {a: 1}
});
zone.run(function() {
expect(Zone.current.get('a')).toBe(1);
setTimeout(function() {
expect(Zone.current.get('a')).toBe(1);
});
});
LifeCycle Interceptable Hooks
- onFork
- onIntercept
- onInvoke
- onHandleError
- onScheduleTask
- onInvokeTask
- onCancelTask
- onHasTask
setTimeout
setTimeout(function() {
console.log('callback invoked');
}, 1000);
setTimeout in Zone
var zone = Zone.current.fork({
name: 'hook',
onScheduleTask: ...,
onInvokeTask: ....,
onHasTask: ...
});
zone.run(() => {
setTimeout(() => {
console.log('timer callback invoked');
}, 100);
});
fork
zone.fork(zoneSpec);
interface ZoneSpec {
name: string;
onScheduleTask: ...,
onInvokeTask: ...,
onCancelTask: ...,
onInvoke: ...,
...,
}
Propagation
const stackTraceZone = Zone.current.fork({
name: 'stackTraceZone',
onScheduleTask: ...
onInvokeTask: ...
});
const logZone = stackTraceZone.fork({
name: 'logZone',
onInvokeTask: ...
});
logZone.run(() => {
setTimeout(...)
});
Propagation
rootZone
stackTraceZone
logZone
onInvokeTask
onInvokeTask
Zone propagation can compose zone behaviors
Compose Parent Behavior
const stackTraceZone = Zone.fork({
name: 'stackTraceZone',
onInvokeTask: (...) => {
console.log('stackTraceZone', Zone.current.name);
return delegate.invokeTask(...)
}
});
const logZone = Zone.fork({
name: 'logZone',
parentZone: stackTraceZone,
onInvokeTask: (...) => {
console.log('logZone', Zone.current.name);
return delegate.invokeTask(...)
}
});
logZone.run(() => {
setTimeout(...)
});
// logZone logZone
// stackTraceZone logZone
Class Inheritance?
class Zone { constructor(public parent, public name) {}}
class StackTraceZone extends Zone {
constructor(parent) { super(parent, 'stackTraceZone');}
onInvokeTask() {
...
this.parent.onInvokeTask();
}
}
class LogZone extends Zone {
constructor(parent) {super(parent, 'logZone');}
onInvokeTask() {
...
this.parent.onInvokeTask();
}
}
const stackTraceZone = new StackTraceZone(rootZone);
const logZone = new LogZone(stackTraceZone);
// logZone logZone
// stackTraceZone stackTraceZone
ZoneDelegate
Zone
ZoneDelegate
name: stackTraceZone
parent: rootZone
name: logZone
run()
parent: stackTraceZone
onInvoke
onInvoke(parentDelegate, ...)
Zone StackFrame
const zoneA = Zone.current.fork({name: 'zoneA'});
const zoneB = Zone.current.fork({name: 'zoneB'});
zoneA.run(() => {
// now we are in zoneA
zoneB.run(() => {
// now we are in zoneB
});
// now we are back in zoneA
...
});
Zone StackFrame
// currentZoneFrame = {parent: null, zone: rootZone}
zoneA.run(() => { // currentZoneFrame = {parent: currentZoneFrame, zone: zoneA}
// now we are in zoneA
zoneB.run(() => { // currentZoneFrame = {parent: currentZoneFrame, zone: zoneB}
// now we are in zoneB
}); // currentZoneFrame = currentZoneFrame.parent
// now we are back in zoneA
...
});
// implementation of Zone.current
get current() {
return currentZoneFrame.zone;
}
currentZoneFrame: ZoneFrame;
interface ZoneFrame {
parent: ZoneFrame;
zone: Zone;
}
Async Error Handling
Zone.current.fork(
{
name: 'error',
onHandleError: (delegate, curr, target, error) => {
logger.error(error);
return delegate.handleError(target, error);
}
}).runGuarded(() => {
setTimeout(() => {
throw new Error('timeout error');
});
setTimeout(() => {
throw new Error('another timeout error');
});
});
Zone.wrap
function testFunc() {
console.log('this in testFunc is:', this);
}
const bindTarget = {};
const boundFunc = testFunc.bind(bindTarget);
boundFunc.apply(newObj);
// ------------------------
//
function testZoneFunc() {
console.log('zoneThis in testFunc is:', Zone.current);
}
const zoneA = Zone.current.fork({name: 'zoneA'});
const wrappedTestZoneFunc = zoneA.wrap(testZoneFunc);
zoneB.run(wrappedTestZoneFunc);
Task
- macroTask
- setTimeout
- setInterval
- XHR
- ...
- microTask
- Promise.then
- eventTask
- addEventListener
- on('event', fn)
zone.scheduleMicrotask
zone.scheduleMicroTask('myOwnMicroTask', callbackFn);
// looks like
Promise.resolve().then(() => {
callbackFn();
});
// zone has it's own microTaskQueue
p.then(() => {});
p.then(() => {});
// without zone, there will be 2 microTasks in
// native microTask queue
// with zone, there will be only 1 microTask in
// native microTask queue
drainMicroTaskQueue() {
zoneMicroTasks.forEach(t => {
t.zone.runTask(t);
});
}
zone microTask queue
zone microTaskQueue
native
microTaskQueue
zoneTask1
zoneTask2
drainMicroTaskQueue
other tasks
zone.scheduleMacroTask('name', callback,
data, customSchedule, customCancel);
zone.scheduleEventTask('name', callback,
data, customSchedule, customCancel);
other methods
Zone.__symbol__ // will return a string starts with __symbol__
Zone.__symbol__('test') === '__zone_symbol__test';
Zone.__load_patch('myOwnAsyncApi',
(global, Zone, api) => {});
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
);
}
When should we use Zone
- Test
- Sync(Disallow Async)
- Async(Auto done, Auto cleanup)
- FakeAsync()
- Debug
- LongStackTrace
- Task Tracing
- Performance measure
- Framework AutoRender
- User Action Tracing
- Resource Releasing
Demo: LongStackTrace
function main () {
b1.addEventListener('click', bindSecondButton);
}
/*
* What if your stack trace could tell you what
* order the user pushed the buttons from the stack trace?
* What if you could log this back to the server?
* Think of how much more productive your debugging could be!
*/
function bindSecondButton () {
b2.addEventListener('click', throwError);
}
function throwError () {
throw new Error('aw shucks');
}
main();
Demo: Tracking: Counting
function btnClicked () {
recurRandonGenerateTimeout(10);
}
function recurRandonGenerateTimeout (x) {
if (x > 0) {
setTimeout(function () {
for (var i = x; i < 8; i++) {
recur(x - 1, Math.random()*100);
}
}, t);
}
}
Performance Profiling
function btnClicked () {
asyncHeavyWork1();
asyncHeavyWork2();
asyncHttpRequest();
}
Auto releasing
const fs = require('fs');
fs.open('./test.txt', 'r', (err, fd) => {
fs.fstat(fd, (err, stat) => {
doSomething(fd, err => {});
doSomethingElse(fd, err => {});
// when to release fd?
});
});
User Action Tracking
viewBtnClicked() {
httpRequest(viewUrl);
httpRequest(additionalInfoUrl);
}
orderBtnClicked() {
httpRequest(orderUrl);
httpRequest(transactionUrl);
}
errorBtnClicked() {
throw new Error();
}
UI Auto Rendering
function httpBtnClicked() {
httpRequestUrl(viewUrl);
}
function timeoutBtnClicked() {
setTimeout(() => {
data.timeout = 'timeout';
});
}
function addBtnClicked() {
if (!data.num) {
data.num = 0;
}
data.num ++;
}
Async Test
const api = require('./async-lib');
describe('testAsync', () => {
it('test async operation', (done) => {
let a = 0;
api.testAsync(a, r => {
expect(r).toBe(1);
done();
});
});
});
const api = require('./async-lib');
describe('testAsync', async(() => {
it('test async operation', () => {
let a = 0;
api.testAsync(a, r => {
expect(r).toBe(1);
});
});
}));
fakeAsync Test
describe('testAsync', () => {
it('test long async operation', (done) => {
setTimeout(() => {
// ... expect something.
done();
}, 10000);
});
});
describe('testAsync', () => {
it('test long async operation', fakeAsync(() => {
setTimeout(() => {
// ... expect something.
}, 10000);
tick(10000);
}));
});
ProxyZoneSpec
describe('testAsync', () => {
it('async', async(() => {
}));
it('fakeAsync', fakeAsync(() => {
}));
});
global.it = function(testFn) {
runTestInProxyZone(testFn);
}
function async() {
const asyncTestZoneSpec = new AsyncTestZoneSpec();
getProxyZoneSpec().setDelegate(asyncTestZoneSpec);
}
ProxyZoneSpec
{delegate: delegateSpec,
onInvokeTask
...
}
realZoneSpec
onInvokeTask
Zone.js in Angular
NgZone
Tell when to trigger Change Detection
AsyncTest
FakeAsyncTest
SyncTest
Error Handling
Debug/Tracing
TaskTrackingZone
LongStackTraceZone
ngZone
ngZone.run(() => {
// will be in angular zone
// will trigger change detection
});
ngZone.runOutsideAngular(() => {
// will be outside angular zone
// "will not trigger change detection"
});
wtfZoneSpec
TaskTrackingZoneSpec
longStackTraceZoneSpec
angular
Features of zone.js in Angular
Module
Timer
Promise
EventTarget
...
requestAnimationFrame
Disable specified Module
(window as any).__Zone_disable_requestAnimationFrame = true;
Ionic v4
(window as any).__Zone_disable_customElements = true;
Disable Specified Event
// recommended variable name
__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove'];
// deprecated variable name
// __zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove'];
Native Delegate
// ngZone.runOutsideAngular(() => {});
declare let Zone: any;
(window as any)[Zone.__symbol__('setTimeout')](() => {
// will not in zone.
}, 100);
Promise
ZoneAwarePromise
- Pass Promise A+ Test
- Support Promise.prototype.finally and Promise.prototype.allSettled
- Support Bluebird
- All Bluebird additional APIs will in the zone
- Will support Promise.any soon
- native async/await not supported :(
Error Handling
import 'zone.js/dist/zone-error';
Error Handling
import 'zone.js/dist/zone-error';
class MyError extends Error {
}
try {
throw new MyError('myError');
} catch (error: Error) {
if (error instanceof MyError) {
// do some special error handling
}
}
(window as any).__Zone_Error_BlacklistedStackFrames_policy
= 'disable';
Electron
- Browser
- NodeJs
- Menu
- Screenshot
- Shell
- Main<->Render communication
import 'zone.js/dist/zone-mix';
import 'zone.js/dist/zone-patch-electron';
More APIs Support
-
MediaQuery
-
Notification
-
RTCPeerConnction
-
ShadyDom
-
Cordova
-
ResizeObserver
-
SocketIO
-
UserMedia
-
Jsonp Helper
FakeAsync: Date.now
will support performance api later
it('should advance Date with tick in fakeAsync',
fakeAsync(() => {
const start = Date.now();
tick(100);
const end = Date.now();
expect(end - start).toBe(100);
}));
FakeAsync: jasmine.clock
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should get date diff correctly', () => {
// automatically run into fake async zone,
const start = Date.now();
jasmine.clock().tick(100);
const end = Date.now();
expect(end - start).toBe(100);
});
__zone_symbol__fakeAsyncAutoFakeAsyncWhenClockPatched
FakeAsync: rxjs.scheduler
import 'zone.js/dist/zone-patch-rxjs-fake-async';
it('rxjs scheduler should be advanced by tick in fakeAsync',
fakeAsync(() => {
observable.delay(1000).subscribe(v => {
result = v;
});
expect(result).toBeNull();
testZoneSpec.tick(1000);
expect(result).toBe('hello');
}));
Better timeout message
it('should timeout', async(() => {
setTimeout(() => {}, 10000);
fixture.whenStable(() => {});
}));
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
--Pendng async tasks are: [type: macroTask, source: setTimeout, args: {handleId:3,isPeriodic:false,delay:10000,args:[]}
Differential Loading
<script src="zone.js/dist/zone-evergreen.js"
type="module"></script>
<script src="zone.js/dist/zone.js" nomodule></script>
Event Bubble Performance
<div (click)="doSomethingElse()">
<button (click)="doSomething()">click me</button>
</div>
platformBrowserDynamic()
.bootstrapModule(
AppModule,
{
ngZoneEventCoalescing: true
}
)
Solution
Event Listeners
button.addEventListener('click', click1);
button.addEventListener('click', click2);
button.addEventListener('mousedown', mousedown1);
const clickListeners = button.eventListeners('click');
// will get click1 and click2
button.removeAllListeners('click');
// will remove click1 and click2
button.removeAllListeners();
// will remove all listeners for all events
Zone.js is merged into angular mono repo and fully built with bazel
Thank you!
Zone.js internal
By jiali
Zone.js internal
- 1,049