AngularJS Internals



AngularJS Internals

0 1


angularjs-internals-slides

Slides for my AngularJS Internals presentation made with reveal.js

On Github cvuorinen / angularjs-internals-slides

AngularJS Internals

– A look behind all the magic

by Carl Vuorinen / @cvuorinen

Carl Vuorinen

HTML enhanced for web apps!

<h4>Hello {{name}}!</h4>
<input ng-model="name">
▶  Run
<h4>Todo:</h4>
<ul ng-init="list=[];">
  <li ng-repeat="todo in list"
      ng-click="list.splice($index, 1)">
    {{todo}}
  </li>
</ul>
<form ng-submit="list.push(todo);todo='';">
  <input ng-model="todo" placeholder="Add">
</form>
▶  Run

Scope

$scope == POJO

Plain Old Javascript Object

$watch & $digest

 red border 
== "pseudo" code
$scope.$watch = function(watchFunc, listenerFunc) {
    this.$$watchers.push({
        watchFunc: watchFunc,
        listenerFunc: listenerFunc
    });
};
$scope.$digest = function() {
    var dirty = true;
    var scope = this;
    
    while (dirty) {
        dirty = false; // reset from last iteration
        
        scope.$$watchers.map(function (watcher) {
            // execute watch function to get new value
            newValue = watcher.watchFunc(scope);
            
            // check if value has changed from last cycle
            if (newValue !== watcher.lastValue) {
                dirty = true;
                
                // execute listener and store new last value
                watcher.listenerFunc(newValue, watcher.lastValue, scope);
                watcher.lastValue = newValue;
            }
        });
    }
};

Scope Inheritance

Javascript prototypical inheritance

function Vehicle() {}
Vehicle.prototype.speed = 4;
Vehicle.prototype.go = function() {
  return 'Vr'
       + 'o'.repeat(this.speed)
       + 'm!';
};

function Truck() {}
Truck.prototype = Vehicle.prototype;

var scania = new Truck();
scania.go();  // Vroooom!
function Car(speed) {
    this.speed = speed;
}
Car.prototype = Object.create(
    Vehicle.prototype
);
Car.prototype.constructor = Car;

var honda = new Car(6);
honda.go();  // Vroooooom!

var tesla = new Car(12);
tesla.go();  // Vroooooooooooom!

Scope Hierarchy

$scope.$new = function() {
    var ChildScope = function() {};
    ChildScope.prototype = this;

    return new ChildScope();
};
<label>
  <input type="checkbox" ng-model="show">
  Show me the Money!
</label><br>
<label ng-if="show">
  <input type="checkbox" ng-model="showMore">
  SHOW MOAR MONEY!!1
</label>
<div ng-if="show">
  <img src="img/money.jpg"><br>
  <img ng-if="showMore"
       src="img/mo-money.jpg">
</div>
▶  Run
Whenever you use ngModel, there’s got to be a dot in there somewhere. If you don’t have a dot, you’re doing it wrong. Miško Hevery (creator of AngularJS)

Controller As Syntax

angular.module('app')
.controller("SomeController", function() {
    this.value = "Hello!";
});
<div ng-controller="SomeController as some">
    {{ some.value }}
</div>

Controller As Syntax

angular.module('app')
.controller("SomeController", function() {
    var vm = this;
    vm.value = "Hello!";
    vm.sayHello = function() {
        alert(vm.value);
    }
});
<div ng-controller="SomeController as vm">
    <input type="text" ng-model="vm.value">
    <button ng-click="vm.sayHello()">Click here</button>
</div>

Expressions

<div ng-init="jsFrameworks = npm.search({tags: ['framework']})">
  <div ng-if="(lastWeekCount = (jsFrameworks | since:lastWeek).length) > 10">
    <h3>Holy smokes, Batman!</h3>
    <p>New JS frameworks since last week: {{ lastWeekCount }}
      <small>
        +{{ (lastWeekCount / jsFrameworks.length * 100) | number:1 }}%
      </small>
    </p>
    <p>Time to <button ng-click="startOver()">Clear the Decks!</button></p>
  </div>
</div>

$parse

is kinda like

$parse = function(expr) {
    return function(scope) {
        with (scope) {
            return eval(expr);
        }
    }
}

... except it's really not

DIY Spreadsheet using $parse

{{ interpolation }}

find {{ and }} characters in DOM text nodes $parse found expressions create a $watch with the parsed expression listener updates DOM node text
+ same thing for attributes

Directives

angular.module('app')
.directive('awesomeButton', function() {
  return {
    scope: {
      click: '&',
      icon: '@'
    },
    transclude: true,
    template: '<button class="button" '+
              '        ng-click="click()">'+
              '<i class="fa fa-{{icon}}"></i>'+
              '<span ng-transclude></span>'+
              '</button>'
  };
});
<div>
    <awesome-button icon="bullhorn"
                    click="my.alert()">
        Click here!
    </awesome-button>
</div>
▶  Run

Compiling & Linking

<div ng-init="arr=['Foo','Bar']">
    <div ng-repeat="item in arr">
        <button ng-click="submit(item)">
                {{ item }}
        </button>
    </div>
</div>
1. compile
2. compile
3. compile
4. compile 5. link
6. link
7. link
8. link

Transclusion

<div>
    <my-directive>
        <p>Some Content</p>
    </my-directive>
</div>
angular.module('app')
.directive('myDirective', function() {
  return {
    transclude: true,
    template: '<div>' +
      '<h4>Title</h4>' +
      '<div ng-transclude></div>' +
      '</div>'
  };
});
take content
insert here
angular.module('ng')
.directive('ngInit', function($parse) {
  return {
    link: function (scope, element, attrs) {
      $parse(attrs.ngInit)(scope);
    }
  };
});
angular.module('ng')
.directive('ngClick', function($parse) {
  return {
    link: function (scope, element, attrs) {
      var expression = $parse(attrs.ngClick);
      element.on('click', function() {
        scope.$apply(function () {
          expression(scope);
        });
      });
    }
  };
});
angular.module('ng')
.directive('ngRepeat', function($parse, $transclude) {
  return {
    transclude: 'element',
    link: function (scope, element, attrs) {
      var match = attrs.ngRepeat.match(/^\s*([\s\S]+?)  ...  \s*$/);
      
      $scope.$watch($parse(match.target), function (collection) {
        // locate existing items, mark items not present for removal
        
        // remove leftover items
        
        // update existing items' scopes
        
        // create new elements with $transclude
        // and keep a reference to their scopes
      });
    }
  };
});
angular.module('ng')
.directive('ngModel', function() {
  return {
    link: function (scope, element, attrs) {
      var currentValue;
      element.on('keyup', function() {
        if ($(element).val() != currentValue) {
          scope.$apply(function () {
            scope[attrs.ngModel] = currentValue = $(element).val();
          });
        }
      });
      
      $scope.$watch(attrs.ngModel, function (newValue) {
        currentValue = newValue;
        $(element).val(newValue);
      });
    }
  };
});

Performance

The amount of data on a Scope does not affect performance.

The number of $watches and the frequency of $digest cycles can slow things down.

30 rows x 50 cols = 1500 cells

Don't call functions that do resource extensive or time consuming computations from watched expressions!

<!-- BAD -->
<label>The Ultimate Question</label>
<input ng-model="question">
Answer: {{ deepThought.compute(question) }}
<!-- LITTLE BETTER -->
<label>The Ultimate Question</label>
<input ng-model="question"
       ng-change="answer = deepThought.compute(question)">
Answer: {{ answer }}
<!-- GOOD -->
<label>The Ultimate Question</label>
<input ng-model="question"
       ng-change="answer = deepThought.compute(question)"
       ng-model-options="{ debounce: 1000 }">
Answer: {{ answer }}

OK, let's look at the original example again

<h4>Todo:</h4>
<ul ng-init="list=[];">
  <li ng-repeat="todo in list"
      ng-click="list.splice($index, 1)">
    {{todo}}
  </li>
</ul>
<form ng-submit="list.push(todo);todo='';">
  <input ng-model="todo" placeholder="Add">
</form>
▶  Run
expression
$watch
$watch
expression
expression + $watch
 expression
by Tero Parviainen

Know Your AngularJS Inside Out

http://teropa.info/build-your-own-angular

Questions?

THE END

AngularJS Internals – A look behind all the magic by Carl Vuorinen / @cvuorinen