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