Run digest cycle in web worker

Dr. Gleb Bahmutov PhD
Kensho Boston / NYC
Personal site http://glebbahmutov.com/
Blog http://glebbahmutov.com/blog/
Twitter @bahmutov

Kensho ♥ Angular

Let us compute primes

Kensho ♥ Angular

function findPrimes(n) {
  var k, primes = [];
  for (k = 0; k < n; k += 1) {
    var prime = findPrime(k + 2);
    primes.push(prime);
  }
  return primes;
}

Kensho ♥ Angular

// page code
var scope = new Scope();
scope.n = 0;

scope.$watch(function watchN(scope) {
  return scope.n;
}, function computePrimes(newVal, oldVal, scope) {
  if (newVal === oldVal) { return; }
  scope.primes = findPrimes(scope.n);
});

document.getElementById('compute').addEventListener('click', 
  function () {
    scope.n += 2000;
    scope.$digest(function (scope) {
      var html = computeMarkup(scope);
      render(html);
    });
  });

Compute!

Kensho ♥ Angular

Coffee scripting

Kensho ♥ Angular

Kensho ♥ Angular

// micro-angular.js
function Scope() {
  this.$$watchers = [];
}
Scope.prototype.$watch = function(watchFn, listenerFn) {
  var watcher = {
    watchFn: watchFn,
    listenerFn: listenerFn || function() { }
  };
  this.$$watchers.push(watcher);
};
Scope.prototype.$digest = function(cb) {
  var self = this;
  var dirty = this.$$watchers.some(function (watch) {
    var newValue = watch.watchFn(self);
    var oldValue = watch.last;
    if (newValue !== oldValue) {
      watch.listenerFn(newValue, oldValue, self);
      watch.last = newValue;
      return true;
    }
  });
  dirty && cb && cb(this);
};

Kensho ♥ Angular

  main context (index.html)  |     worker.js
-----------------------------|---------------------------------------
   loads mock-scopes.js      | loads micro-angular.js and primes.js
   client uses mockScopes    | actual scopes
   loads worker.js           | $digest returns markup text

Move model to a web worker

Digest takes too long?

Kensho ♥ Angular

Kensho ♥ Angular

// index.html app code
var scope = new Scope();
var localN = 0;
scope.set('n', 0);

scope.$watch(function (scope) {
  return scope.n;
}, function (newVal, oldVal, scope) {
  if (newVal === oldVal) { return; }
  scope.primes = findPrimes(scope.n);
});

document.getElementById('compute').addEventListener('click', 
  function () {
    localN += 2000;
    scope.set(n, localN);
    scope.$digest(function (scope) {
      render(computeMarkup(scope));
    });
  });

Kensho ♥ Angular

Web worker micro angular

Kensho ♥ Angular

Client-side mock scopes

function Scope() {
  this.id = '$' + scopes;
  scopes += 1;
  digestWorker.postMessage({
    cmd: 'Scope',
    id: this.id
  });
  console.log('created mock scope', this.id);
}
Scope.prototype.set = function (name, value) {
  digestWorker.postMessage({
    cmd: 'set',
    id: this.id,
    name: name,
    value: value
  });
  console.log('set mock scope', this.id, 'property', name, '=', value);
};
Scope.prototype.$watch = function (watchFn, listenerFn) {
  digestWorker.postMessage({
    cmd: '$watch',
    id: this.id,
    watchFn: watchFn.toString(),
    listenerFn: listenerFn && listenerFn.toString()
  });
};
Scope.prototype.$digest = function ($compile) { ... };

Kensho ♥ Angular

Worker adapter

importScripts('micro-angular.js', 'primes.js');
var scopes = {};
onmessage = function digestOnMessage(e) {
  switch (e.data.cmd) {
    case 'Scope':
      scopes[e.data.id] = new Scope(e.data.id);
    break;
    case 'set':
      scopes[e.data.id][e.data.name] = e.data.value;
    break;
    case '$watch': 
      ...
    break;
    case '$digest':
      scopes[e.data.id].$digest(function digestFinished() {
        var compiled = eval('(' + e.data.$compile + ')');
        var scope = scopes[e.data.id];
        var html = compiled(scope);
        postMessage({
          cmd: 'digestFinished',
          html: html
        });
      });
    break;
  }
};

Kensho ♥ Angular

Could we?

importScripts('micro-angular.js', 'primes.js');
var scopes = {};
onmessage = function digestOnMessage(e) {
  switch (e.data.cmd) {
    case 'Scope':
      scopes[e.data.id] = new Scope(e.data.id);
    break;
    case 'set':
      scopes[e.data.id][e.data.name] = e.data.value;
    break;
    case '$watch': 
      ...
    break;
    case '$digest':
      scopes[e.data.id].$digest(function digestFinished() {
        var compiled = eval('(' + e.data.$compile + ')');
        var scope = scopes[e.data.id];
        var html = compiled(scope);
        var diff = virtualDomDiffs(prevDom, newDom);
        postMessage({
          cmd: 'digestFinished',
          diff: diff
        });
      });
    break;
  }
};

Kensho ♥ Angular

Restore POJO

// BAD
scope.set('n', 0);
// GOOD
scope.n = 0;
// mock scope client-side
function Scope() {
    digestWorker.postMessage({
      cmd: 'Scope',
      id: this.id
    });
    var self = this;
    Object.observe(this, function (changes) {
      changes.forEach(function (change) {
        switch (change.type) {
          case 'add':
          case 'update':
            console.log('change', change.name, 'to', change.object[change.name]);
            self.set(change.name, change.object[change.name]);
          break;
        }
      });
    });
  }

Kensho ♥ Angular

Restored POJO working

Dr. Gleb Bahmutov PhD
Kensho Boston / NYC
Personal site http://glebbahmutov.com/
Blog http://glebbahmutov.com/blog/
Twitter @bahmutov

Kensho ♥ Angular

Copy of Run digest cycle in web worker

By Praveen Poonia

Copy of Run digest cycle in web worker

An experiment in offloading the Angular dirty checking and model update step to a separate browser thread. ng-conf 2015, Salt Lake City, Utah

  • 1,001