Zone.js in Depth

@JiaLiPassion

Who am I

  • Name: Jia Li
  • Company: ThisDot
  • Zone.js: Code Owner
  • Angular: Collaborator

Agenda

  • What's Zone.js
  • When and why you need Zone.js
  • 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

What is execution context

execution context is an abstract concept that holds information about the environment within which the current code is being executed

Execution Context

const globalThis = this;

function testFunc() {
  console.log('this in testFunc is:', this === globalThis);
}

testFunc();

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 JS

  • When code is executed in a function, there will be a new execution context
  • Determine Scope (variables and functions the function can access)  
  • Determine the value of `this`.

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);
  });
});

Execution Context in Zone.js

  • It looks like we have a new `zoneThis` magic variable like `this`.
  • for sync operation, `zoneThis` will be the zone it is running in.
  • for async operation, `zoneThis` will be the zone it is scheduled.

How to get `zoneThis`

zone.run(function() {
  expect(Zone.current).toBe(zone);
  setTimeout(function() {
    expect(Zone.current).toBe(zone);
  });
});

LifeCycle Interceptable Hooks

  • onFork
  • onIntercept
  • onInvoke
  • onHandleError
  • onScheduleTask
  • onInvokeTask
  • onCancelTask
  • onHasTask

setTimeout

setTimeout(function() {
  console.log('callback invoked');
}, 1000);

Async in Javascript Engine

// Javascirpt 101
function a() {}
function b() {}
function c() {}
function d() {}

a();
setTimeout(c, 100);
setTimeout(d, 100);
b();
// run order
// 1. a
// 2. b
// 3. c
// 4. d

performance counter

// Javascirpt 101
function a() {// cost 100ms }
function b() {// cost 100ms }
function c() {// cost 100ms }
function d() {// cost 100ms }

performance.start();
a();
setTimeout(c, 100);
setTimeout(d, 200);
b();
performance.end();

Async in Javascript

Time

a

b

c

d

performance.start()

performance.end()

setTimeout in Zone

var zone = Zone.current.fork({
  name: 'hook',
  onScheduleTask: ...,
  onInvokeTask: ....,
  onHasTask: ...
});
zone.run(() => {
  setTimeout(() => {
    console.log('timer callback invoked');
  }, 100);
});

Zone

zoneA = {
  onInvokeTask: (callback) => {
    performance.start();
    callback();
    performance.end();
  }
};
zoneA.run(() => {
  performance.start();
  a();
  setTimeout(c);
  setTimeout(d);
  b();
  performance.end();
});
setTimeout(e);

Zone

Time

a

b

c

performance.start()

performance.end()

performance.start()

performance.end()

d

performance.start()

performance.end()

e

zoneA

zoneA

zoneA

zoneA

Hooks

  • onScheduleTask
  • onInvokeTask
  • onCancelTask
  • onInvoke

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.current

console.log('root zone:', Zone.current.name);
Zone.current.fork({name: 'logZone'}).run(() => {
  console.log('root zone:', Zone.current.name);
});

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

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();
});

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

const api = {
  add: function add(a, b) {
    return a + b;
  }
}

api.add(1, 2); // will return 3;

const originalAdd = api.add;
api.add = function() {
  console.log('api.add is called');
  return originalAdd.apply(this, arguments);
}

api.add(1, 2); // will log and return 3;

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) => {
  if (err) throw err;
  fs.fstat(fd, (err, stat) => {
    if (err) throw err;
    doSomething(fd, err => {});
    doSomethingElse(fd, err => {});
  });
});

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);
  }));
});

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
});

Features of zone.js you may not know

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
  • Support Bluebird
    • All Bluebird specified APIs will in zone
  • Will support Promise.any and Promise.allSettled

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:[]}

Jasmine 3.x/Mocha 5.x

Will support Jest!

  • Better error message
  • Better jest timer(useFakeTimers/runAllTicks/etc...) support

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 !!!

Thank you!

Zone.js in depth

By jiali

Zone.js in depth

  • 1,222