Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lightweight AngularJS

Minko Gechev
November 29, 2014

Lightweight AngularJS

Light implementation of AngularJS.

Implement your own AngularJS in ~200 lines of code.

Minko Gechev

November 29, 2014
Tweet

More Decks by Minko Gechev

Other Decks in Programming

Transcript

  1. Lightweight AngularJS
    Minko Gechev

    View Slide

  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"]
    }

    View Slide

  3. View Slide

  4. Table of Contents
    • Introduction to AngularJS
    • Light AngularJS
    • DOM compiler
    • Provider
    • Scope
    • Services
    • Directives

    View Slide

  5. Purpose of the presentation
    • Learn how AngularJS works by building a
    simplified version of it
    • Understand design decisions behind the
    implementation of AngularJS

    View Slide

  6. View Slide

  7. Introduction to AngularJS

    View Slide

  8. View Slide

  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

    View Slide

  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.

    View Slide






  11. {{buttonText}}



    View Slide

  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.

    View Slide

  13. function MyController($scope) {
    $scope.buttonText = 'Click me to change foo!';
    $scope.foo = 42;
    $scope.changeFoo = function () {
    $scope.foo += 1;
    alert('Foo changed');
    };
    }

    View Slide

  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.

    View Slide



  15. ng-click="foo()">Parent method
    Child method


    <br/>function BaseCtrl($scope) {<br/>$scope.foo = function () {<br/>alert('Base foo');<br/>};<br/>}<br/>function ChildCtrl($scope) {<br/>$scope.bar = function () {<br/>alert('Child bar');<br/>};<br/>}<br/>

    View Slide

  16. Directives
    In AngularJS the directives are the place where all
    DOM manipulations should be placed.

    View Slide

  17. <br/>myModule.directive('alertButton', function () {<br/>return {<br/>template: '<button ng-transclude></button>',<br/>scope: {<br/>content: '@'<br/>},<br/>replace: true,<br/>restrict: 'E',<br/>transclude: true,<br/>link: function (scope, el) {<br/>el.click(function () {<br/>alert(scope.content);<br/>});<br/>}<br/>};<br/>});<br/>
    Click me

    View Slide

  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.

    View Slide

  19. <br/>myModule.filter('uppercase', function () {<br/>return function (str) {<br/>return (str || '').toUpperCase();<br/>};<br/>});<br/>
    {{ name | uppercase }}
    <br/>function MyCtrl($scope, uppercaseFilter) {<br/>$scope.name = uppercaseFilter('foo'); //FOO<br/>}<br/>

    View Slide

  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.

    View Slide

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

    View Slide

  22. Light AngularJS implementation

    View Slide

  23. Restrictions
    • Our light AngularJS provides basic support of only:
    • Controllers
    • Directives
    • Services
    • Scope

    View Slide

  24. And is on only ~200 SLOC

    View Slide

  25. DOMCompiler
    Provider Scope

    View Slide

  26. Provider
    Responsibilities:
    • Register components (directives, services,
    controllers)
    • Resolves dependencies of the components
    • Initialize the components

    View Slide

  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

    View Slide

  28. DOMCompiler
    Responsibilities:
    • Compiles the DOM
    • Traverse the whole DOM tree
    • Finds registered directives
    • Invokes the logic associated with them
    • Manages the current scope

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  32. View Slide

  33. Lets start one by one

    View Slide

  34. Provider

    View Slide

  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;
    }
    //...

    View Slide

  36. Sample calls
    Provider.directive('ng-bind', function () {
    return {
    link: function (scope) {
    //...
    }
    };
    });
    Provider.directive('ng-repeat', function () {
    return {
    scope: true,
    link: function (scope) {
    //...
    }
    };
    });

    View Slide

  37. Creating components…

    View Slide

  38. But before that…a little theory…

    View Slide

  39. Graph theory

    View Slide

  40. Why?!

    View Slide

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

    View Slide

  42. Graph is…
    Ordered pair of two sets:
    G = { V, E }, E ⊆ VxV

    View Slide

  43. Or simply…
    A
    B
    E
    D
    C

    View Slide

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

    View Slide

  45. HTML








    View Slide

  46. DOM Tree
    html
    head body
    p div

    View Slide

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

    View Slide

  48. A typical problems in graph theory
    • Check whether given vertex has specific property
    • Visit a subset of the the vertices of given graph
    • …

    View Slide

  49. How to find all elements
    with associated directive
    in given DOM tree?

    View Slide

  50. DOM Tree === Acyclic undirected graph

    View Slide

  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

    View Slide

  52. DFS!

    View Slide

  53. View Slide

  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.

    View Slide

  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

    View Slide

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

    View Slide

  57. Lets go back to our implementation

    View Slide

  58. Provider…get a
    service

    View Slide

  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: {}

    View Slide

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

    View Slide

  61. View Slide

  62. Before minification
    this.annotate(function ($http, $q, $browser) {
    //...
    });
    // [“$http”, “$q”, “$browser”]

    View Slide

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

    View Slide

  64. one more problem…

    View Slide

  65. A
    E
    C
    B
    D

    View Slide

  66. A
    E
    C
    B
    D

    View Slide

  67. a cycle…

    View Slide

  68. View Slide

  69. upsss….

    View Slide

  70. View Slide

  71. DOMCompiler

    View Slide

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

    View Slide

  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;
    }
    //...

    View Slide

  74. Scope

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  78. View Slide

  79. Dirty Checking

    View Slide

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

    View Slide

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

    View Slide

  82. and a few directives…

    View Slide

  83. Provider.directive('ngl-click', function () {
    'use strict';
    return {
    scope: false,
    link: function (el, scope, exp) {
    el.onclick = function () {
    scope.$eval(exp);
    scope.$digest();
    };
    }
    };
    });

    View Slide

  84. one-way binding…

    View Slide

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

    View Slide

  86. two-way binding

    View Slide

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

    View Slide

  88. lets take a look at the
    following snippet…

    View Slide

  89. setTimeout & setInterval
    <br/>function Ctrl($scope) {<br/>$scope.counter = 0;<br/>setTimeout(function () {<br/>$scope.counter += 1;<br/>}, 1000);<br/>}<br/>



    View Slide

  90. how AngularJS knows
    count has been changed?

    View Slide

  91. …it doesn’t

    View Slide

  92. setTimeout & setInterval
    <br/>function Ctrl($scope) {<br/>$scope.counter = 0;<br/>setTimeout(function () {<br/>$scope.counter += 1;<br/>$scope.$digest(); //Note that in AngularJS<br/>you should use $apply<br/>}, 1000);<br/>}<br/>



    View Slide

  93. $timeout
    Provider.service('timeout',
    function ($rootScope) {
    'use strict’;
    return function (fn, timeout) {
    setTimeout(function () {
    fn();
    $rootScope.$digest();
    }, timeout);
    };
    });

    View Slide

  94. how about 2k-3k
    bindings?

    View Slide

  95. then dirty checking is
    not reliable…

    View Slide

  96. View Slide

  97. Object.observe
    Already in Chrome

    View Slide

  98. View Slide

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

    View Slide

  100. Resources
    • Source code of AngularJS
    • AngularJS tutorial
    • AngularJS Dev Guide
    • Source code from the slides
    • AngularJS in Patterns
    • AngularJS style guide

    View Slide