Lightweight AngularJS

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=47 Minko Gechev
November 29, 2014

Lightweight AngularJS

Light implementation of AngularJS.

Implement your own AngularJS in ~200 lines of code.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

November 29, 2014
Tweet

Transcript

  1. Lightweight AngularJS Minko Gechev

  2. About me Freelancer, passionate about the web! github.com/mgechev twitter.com/mgechev {

    "name": "Minko Gechev", "job": "Software Engineer", "responsibilities": ["development", "oo design", "trainings", “consultancy”], "interests": ["javascript", "webrtc", "algorithms", "data structures", "design patterns", "karate"] }
  3. None
  4. Table of Contents • Introduction to AngularJS • Light AngularJS

    • DOM compiler • Provider • Scope • Services • Directives
  5. Purpose of the presentation • Learn how AngularJS works by

    building a simplified version of it • Understand design decisions behind the implementation of AngularJS
  6. None
  7. Introduction to AngularJS

  8. None
  9. AngularJS AngularJS is a JavaScript framework developed by Google. It

    intends to provide a solid base for the development of CRUD Single-Page Applications (SPA). • two-way data binding • dependency injection • separation of concerns • testability • abstraction
  10. Partials The partials are HTML strings. They may contain AngularJS

    expressions inside the elements or their attributes. One of the distinctions between AngularJS and the others frameworks is the fact that AngularJS' templates are not in an intermediate format which needs to be turned into HTML.
  11. <html ng-app> <!-- Body tag augmented with ngController directive -->

    <body ng-controller="MyController"> <input ng-model="foo" value="bar"> <!-- Button tag with ng-click directive, and string expression 'buttonText' wrapped in "{{ }}" markup --> <button ng-click="changeFoo()">{{buttonText}}</button> <script src="angular.js"></script> </body> </html>
  12. Controllers The AngularJS controllers are JavaScript functions which help handling

    the user interactions with the web application (for example mouse events, keyboard events, etc.) by attaching methods to the scope. All required external components, for the controllers are provided through the Dependency Injection mechanism of AngularJS.
  13. function MyController($scope) { $scope.buttonText = 'Click me to change foo!';

    $scope.foo = 42; $scope.changeFoo = function () { $scope.foo += 1; alert('Foo changed'); }; }
  14. Scope In AngularJS the scope is a JavaScript object which

    is exposed to the partials. The scope could contains different properties - primitives, objects or methods. All methods attached to the scope could be invoked by evaluation of AngularJS expression inside the partials associated with the given scope, or direct call of the method by any component which keeps reference to the scope.
  15. <div ng-controller="BaseCtrl"> <div id="child" ng-controller="ChildCtrl"> <button id=“parent-method" ng-click="foo()">Parent method</button> <button

    ng-click="bar()">Child method</button> </div> </div> <script> function BaseCtrl($scope) { $scope.foo = function () { alert('Base foo'); }; } function ChildCtrl($scope) { $scope.bar = function () { alert('Child bar'); }; } </script>
  16. Directives In AngularJS the directives are the place where all

    DOM manipulations should be placed.
  17. <script> myModule.directive('alertButton', function () { return { template: '<button ng-transclude></button>',

    scope: { content: '@' }, replace: true, restrict: 'E', transclude: true, link: function (scope, el) { el.click(function () { alert(scope.content); }); } }; }); </script> <alert-button content="42">Click me</alert-button>
  18. Filters The filters in AngularJS are responsible for encapsulating logic

    required for formatting data. Usually filters are used inside the partials but they are also accessible in the controllers, directives, services and other filters through Dependency Injection.
  19. <script> myModule.filter('uppercase', function () { return function (str) { return

    (str || '').toUpperCase(); }; }); </script> <div>{{ name | uppercase }}</div> <script> function MyCtrl($scope, uppercaseFilter) { $scope.name = uppercaseFilter('foo'); //FOO } </script>
  20. Services Every piece of logic which doesn't belong to the

    components described above should be placed inside a service. Usually services encapsulate the domain specific logic, persistence logic, XHR, WebSockets, etc.
  21. myModule.factory('Developer', function () { return function Developer() { this.name =

    'Foo'; this.motherLanguage = 'JavaScript'; this.code = function () {}; this.live = function () { while (true) { this.code(); } }; }; }); function MyCtrl(Developer) { var developer = new Developer(); developer.live(); }
  22. Light AngularJS implementation

  23. Restrictions • Our light AngularJS provides basic support of only:

    • Controllers • Directives • Services • Scope
  24. And is on only ~200 SLOC

  25. DOMCompiler Provider Scope

  26. Provider Responsibilities: • Register components (directives, services, controllers) • Resolves

    dependencies of the components • Initialize the components
  27. Provider interface • get(name,  locals) - returns given service  

    • invoke(fn,  locals) - initialize given service   • directive(name,  fn) - registers a directive   • controller(name,  fn) - registers a controller   • service(name,  fn) - registers a service   • annotate(fn) - returns an array of the dependencies of given service
  28. DOMCompiler Responsibilities: • Compiles the DOM • Traverse the whole

    DOM tree • Finds registered directives • Invokes the logic associated with them • Manages the current scope
  29. DOMCompiler interface • bootstrap() - bootstraps the app   •

    callDirectives(el,  scope) - invokes directive associated to given element   • compile(el,  scope) - compiles the subtree with root given element
  30. Scope • Watches expressions • Evaluates all watched expressions on

    each $digest loop • Invokes the associated logic on change of the expression result, compared to the previous call of $digest
  31. Scope interface • $watch(exp,  fn) - associates given function to

    an expression   • $eval(exp) - evaluates given expression   • $new() - creates new scope, child of the current   • $destroy() - destroys given scope   • $digest() - performs dirty checking
  32. None
  33. Lets start one by one

  34. Provider

  35. Register Components //... directive: function (name, fn) { this._register(name +

    Provider.DIRECTIVES_SUFFIX, fn); }, controller: function (name, fn) { this._register(name + Provider.CONTROLLERS_SUFFIX, function () { return fn; }); }, service: function (name, fn) { this._register(name, fn); }, _register: function (name, factory) { this._providers[name] = factory; } //...
  36. Sample calls Provider.directive('ng-bind', function () { return { link: function

    (scope) { //... } }; }); Provider.directive('ng-repeat', function () { return { scope: true, link: function (scope) { //... } }; });
  37. Creating components…

  38. But before that…a little theory…

  39. Graph theory

  40. Why?!

  41. Says: “Formalize all the things!!” FTW!

  42. Graph is… Ordered pair of two sets: G = {

    V, E }, E ⊆ VxV
  43. Or simply… A B E D C

  44. Why the f#%& we need math here?

  45. HTML <html> <head> </head> <body> <p></p> <div></div> </body> </html>

  46. DOM Tree html head body p div

  47. Dependency graph $resource $q $http $httpBackend $browser

  48. A typical problems in graph theory • Check whether given

    vertex has specific property • Visit a subset of the the vertices of given graph • …
  49. How to find all elements with associated directive in given

    DOM tree?
  50. DOM Tree === Acyclic undirected graph

  51. Traverse the DOM tree Two basic algorithms: • Breath-First Search

    (BFS) • Finds the shortest path between nods • Depth-First Search (DFS) • Finds any path
  52. DFS!

  53. None
  54. We can represent graphs by… • A matrix A, where

    A[i][j] will be equals to 1 if and only if there is a path between i and j, otherwise it’ll be equals to 0.
  55. Example… 0 3 4 1 2 0 1 1 1

    0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0
  56. DFS JavaScript implementation function dfs(graph, current, visited) { 'use strict';

    visited = visited || []; if (visited[current]) { return; } visited[current] = true; for (var k = 0; k < graph.length; k += 1) { if (graph[current][k] && !visited[k]) { dfs(graph, k, visited); } } }
  57. Lets go back to our implementation

  58. Provider…get a service

  59. directive: function (name, fn) { … }, controller: function (name,

    fn) { … }, service: function (name, fn) { … }, _register: function (name, factory) { … }, get: function (name, locals) { if (this._cache[name]) { return this._cache[name]; } var provider = this._providers[name]; if (!provider || typeof provider !== 'function') { return null; } return (this._cache[name] = this.invoke(provider, locals)); }, _cache: {}
  60. annotate: function (fn) { var res = fn.toString() .replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '')

    .match(/\((.*?)\)/); if (res && res[1]) { return res[1].split(',').map(function (d) { return d.trim(); }); } return []; }, invoke: function (fn, locals) { locals = locals || {}; var deps = this.annotate(fn).map(function (s) { return locals[s] || this.get(s, locals); // Indirect recursion }, this); return fn.apply(null, deps); }
  61. None
  62. Before minification this.annotate(function ($http, $q, $browser) { //... }); //

    [“$http”, “$q”, “$browser”]
  63. After minification… this.annotate(function (a, b, c) { //... }); //

    [“a”, “b”, “c”]
  64. one more problem…

  65. A E C B D

  66. A E C B D

  67. a cycle…

  68. None
  69. upsss….

  70. None
  71. DOMCompiler

  72. //... compile: function (el, scope) { scope = this.callDirectives(el, scope);

    [].forEach.call(el.children || [], function (d) { this.compile(d, scope); // Recursive call }.bind(this)); } //...
  73. //... callDirectives: function (el, $scope) { var isCreated = false;

    [].map.call(el.attributes || [], function (attr) { var directive = Provider.get(attr.name + Provider.DIRECTIVES_SUFFIX); return directive && { expr: attr.value, provision: directive }; }) // takes all expressions with value evaluated to true // i.e. if such directive exists the literal will // be returned .filter(Boolean) .forEach(function (d) { if (d.provision.scope && !isCreated) { isCreated = true; $scope = $scope.$new(); } d.provision.link(el, $scope, d.expr); }); return $scope; } //...
  74. Scope

  75. $rootScope $scope0 $scope1 $scope3 $scope4 $scope2

  76. function Scope(parent, id) { 'use strict'; this.$$watchers = []; this.$$children

    = []; this.$parent = parent; this.$id = id || 0; }
  77. Scope.prototype.$new = function () { 'use strict'; Scope.counter += 1;

    var obj = new Scope(this, Scope.counter); Object.setPrototypeOf(obj, this); this.$$children.push(obj); return obj; }; Scope.prototype.$destroy = function () { 'use strict'; var pc = this.$parent.$$children; pc.splice(pc.indexOf(this), 1); };
  78. None
  79. Dirty Checking

  80. Scope.prototype.$watch = function (exp, fn) { 'use strict'; this.$$watchers.push({ exp:

    exp, fn: fn, last: Utils.clone(this.$eval(exp)) }); };
  81. Scope.prototype.$digest = function () { 'use strict'; var dirty, watcher,

    current, i; do { dirty = false; for (i = 0; i < this.$$watchers.length; i += 1) { watcher = this.$$watchers[i]; current = this.$eval(watcher.exp); if (!Utils.equals(watcher.last, current)) { watcher.last = Utils.clone(current); dirty = true; watcher.fn(current); } } } while (dirty); for (i = 0; i < this.$$children.length; i += 1) { this.$$children[i].$digest(); // Recursive call } };
  82. and a few directives…

  83. Provider.directive('ngl-click', function () { 'use strict'; return { scope: false,

    link: function (el, scope, exp) { el.onclick = function () { scope.$eval(exp); scope.$digest(); }; } }; });
  84. one-way binding…

  85. Provider.directive('ngl-bind', function () { 'use strict'; return { scope: false,

    link: function (el, scope, exp) { el.innerHTML = scope.$eval(exp); scope.$watch(exp, function (val) { el.innerHTML = val; }); } }; });
  86. two-way binding

  87. Provider.directive('ngl-model', function () { 'use strict'; return { link: function

    (el, scope, exp) { el.onkeyup = function () { scope[exp] = el.value; scope.$digest(); }; scope.$watch(exp, function (val) { el.value = val; }); } }; });
  88. lets take a look at the following snippet…

  89. setTimeout & setInterval <script> function Ctrl($scope) { $scope.counter = 0;

    setTimeout(function () { $scope.counter += 1; }, 1000); } </script> <div ng-controller="Ctrl"> <span ng-bind="counter"></span> </div>
  90. how AngularJS knows count has been changed?

  91. …it doesn’t

  92. setTimeout & setInterval <script> function Ctrl($scope) { $scope.counter = 0;

    setTimeout(function () { $scope.counter += 1; $scope.$digest(); //Note that in AngularJS you should use $apply }, 1000); } </script> <div ng-controller="Ctrl"> <span ng-bind="counter"></span> </div>
  93. $timeout Provider.service('timeout', function ($rootScope) { 'use strict’; return function (fn,

    timeout) { setTimeout(function () { fn(); $rootScope.$digest(); }, timeout); }; });
  94. how about 2k-3k bindings?

  95. then dirty checking is not reliable…

  96. None
  97. Object.observe Already in Chrome

  98. None
  99. Thank you! github.com/mgechev twitter.com/mgechev

  100. Resources • Source code of AngularJS • AngularJS tutorial •

    AngularJS Dev Guide • Source code from the slides • AngularJS in Patterns • AngularJS style guide