Skip to content

Commit

Permalink
revert: feat(*): lazy one-time binding support
Browse files Browse the repository at this point in the history
This reverts commit cee429f.

See angular#7700 for a more performant approach for bind-once.
  • Loading branch information
rodyhaddad committed Jul 2, 2014
1 parent 63e8952 commit be98f7c
Show file tree
Hide file tree
Showing 13 changed files with 15 additions and 541 deletions.
119 changes: 0 additions & 119 deletions docs/content/guide/expression.ngdoc
Original file line number Diff line number Diff line change
Expand Up @@ -200,122 +200,3 @@ expose a `$event` object within the scope of that expression.

Note in the example above how we can pass in `$event` to `clickMe`, but how it does not show up
in `{{$event}}`. This is because `$event` is outside the scope of that binding.


## One-time binding

An expression that starts with `::` is considered a one-time expression. One-time expressions
will stop recalculating once they are stable, which happens after the first digest if the expression
result is a non-undefined value (see value stabilization algorithm below).

<example module="oneTimeBidingExampleApp">
<file name="index.html">
<div ng-controller="EventController">
<button ng-click="clickMe($event)">Click Me</button>
<p id="one-time-binding-example">One time binding: {{::name}}</p>
<p id="normal-binding-example">Normal binding: {{name}}</p>
</div>
</file>
<file name="script.js">
angular.module('oneTimeBidingExampleApp', []).
controller('EventController', ['$scope', function($scope) {
var counter = 0;
var names = ['Igor', 'Misko', 'Chirayu', 'Lucas'];
/*
* expose the event object to the scope
*/
$scope.clickMe = function(clickEvent) {
$scope.name = names[counter % names.length];
counter++;
};
}]);
</file>
<file name="protractor.js" type="protractor">
it('should freeze binding after its value has stabilized', function() {
var oneTimeBiding = element(by.id('one-time-binding-example'));
var normalBinding = element(by.id('normal-binding-example'));

expect(oneTimeBiding.getText()).toEqual('One time binding:');
expect(normalBinding.getText()).toEqual('Normal binding:');
element(by.buttonText('Click Me')).click();

expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
expect(normalBinding.getText()).toEqual('Normal binding: Igor');
element(by.buttonText('Click Me')).click();

expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
expect(normalBinding.getText()).toEqual('Normal binding: Misko');

element(by.buttonText('Click Me')).click();
element(by.buttonText('Click Me')).click();

expect(oneTimeBiding.getText()).toEqual('One time binding: Igor');
expect(normalBinding.getText()).toEqual('Normal binding: Lucas');
});
</file>
</example>


### Why this feature

The main purpose of one-time binding expression is to provide a way to create a binding
that gets deregistered and frees up resources once the binding is stabilized.
Reducing the number of expressions being watched makes the digest loop faster and allows more
information to be displayed at the same time.


### Value stabilization algorithm

One-time binding expressions will retain the value of the expression at the end of the
digest cycle as long as that value is not undefined. If the value of the expression is set
within the digest loop and later, within the same digest loop, it is set to undefined,
then the expression is not fulfilled and will remain watched.

1. Given an expression that starts with `::` when a digest loop is entered and expression
is dirty-checked store the value as V
2. If V is not undefined mark the result of the expression as stable and schedule a task
to deregister the watch for this expression when we exit the digest loop
3. Process the digest loop as normal
4. When digest loop is done and all the values have settled process the queue of watch
deregistration tasks. For each watch to be deregistered check if it still evaluates
to value that is not `undefined`. If that's the case, deregister the watch. Otherwise
keep dirty-checking the watch in the future digest loops by following the same
algorithm starting from step 1


### How to benefit from one-time binding

When interpolating text or attributes. If the expression, once set, will not change
then it is a candidate for one-time expression.

```html
<div name="attr: {{::color}}">text: {{::name}}</div>
```

When using a directive with bidirectional binding and the parameters will not change

```js
someModule.directive('someDirective', function() {
return {
scope: {
name: '=',
color: '@'
},
template: '{{name}}: {{color}}'
};
});
```

```html
<div some-directive name=“::myName” color=“My color is {{::myColor}}”></div>
```


When using a directive that takes an expression

```html
<ul>
<li ng-repeat="item in ::items">{{item.name}};</li>
</ul>
```

4 changes: 0 additions & 4 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1515,7 +1515,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parentSet(scope, parentValue = isolateScope[scopeName]);
}
}
parentValueWatch.$$unwatch = parentGet.$$unwatch;
return lastValue = parentValue;
}, null, parentGet.literal);
break;
Expand Down Expand Up @@ -1853,9 +1852,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return function textInterpolateLinkFn(scope, node) {
var parent = node.parent(),
bindings = parent.data('$binding') || [];
// Need to interpolate again in case this is using one-time bindings in multiple clones
// of transcluded templates.
interpolateFn = $interpolate(text);
bindings.push(interpolateFn);
parent.data('$binding', bindings);
if (!hasCompileParent) safeAddClass(parent, 'ng-binding');
Expand Down
6 changes: 1 addition & 5 deletions src/ng/directive/ngBind.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,7 @@ var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
element.addClass('ng-binding').data('$binding', attr.ngBindHtml);

var parsed = $parse(attr.ngBindHtml);
function getStringValue() {
var value = parsed(scope);
getStringValue.$$unwatch = parsed.$$unwatch;
return (value || '').toString();
}
function getStringValue() { return (parsed(scope) || '').toString(); }

scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
element.html($sce.getTrustedHtml(parsed(scope)) || '');
Expand Down
3 changes: 0 additions & 3 deletions src/ng/interpolate.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,16 @@ function $InterpolateProvider() {


try {
interpolationFn.$$unwatch = true;
for (; i < ii; i++) {
val = getValue(parseFns[i](context));
if (allOrNothing && isUndefined(val)) {
interpolationFn.$$unwatch = undefined;
return;
}
val = stringify(val);
if (val !== lastValues[i]) {
inputsChanged = true;
}
values[i] = val;
interpolationFn.$$unwatch = interpolationFn.$$unwatch && parseFns[i].$$unwatch;
}

if (inputsChanged) {
Expand Down
41 changes: 3 additions & 38 deletions src/ng/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -1021,21 +1021,13 @@ function $ParseProvider() {
$parseOptions.csp = $sniffer.csp;

return function(exp) {
var parsedExpression,
oneTime;
var parsedExpression;

switch (typeof exp) {
case 'string':

exp = trim(exp);

if (exp.charAt(0) === ':' && exp.charAt(1) === ':') {
oneTime = true;
exp = exp.substring(2);
}

if (cache.hasOwnProperty(exp)) {
return oneTime ? oneTimeWrapper(cache[exp]) : cache[exp];
return cache[exp];
}

var lexer = new Lexer($parseOptions);
Expand All @@ -1048,40 +1040,13 @@ function $ParseProvider() {
cache[exp] = parsedExpression;
}

return oneTime || parsedExpression.constant ? oneTimeWrapper(parsedExpression) : parsedExpression;
return parsedExpression;

case 'function':
return exp;

default:
return noop;
}

function oneTimeWrapper(expression) {
var stable = false,
lastValue;
oneTimeParseFn.literal = expression.literal;
oneTimeParseFn.constant = expression.constant;
oneTimeParseFn.assign = expression.assign;
return oneTimeParseFn;

function oneTimeParseFn(self, locals) {
if (!stable) {
lastValue = expression.constant && lastValue ? lastValue : expression(self, locals);
oneTimeParseFn.$$unwatch = isDefined(lastValue);
if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) {
self.$$postDigestQueue.push(function () {
// create a copy if the value is defined and it is not a $sce value
if ((stable = isDefined(lastValue)) &&
(lastValue === null || !lastValue.$$unwrapTrustedValue)) {
lastValue = copy(lastValue, null);
}
});
}
}
return lastValue;
}
}
};
}];
}
35 changes: 10 additions & 25 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,14 @@ function $RootScopeProvider(){
watcher.fn = function(newVal, oldVal, scope) {listenFn(scope);};
}

if (typeof watchExp == 'string' && get.constant) {
var originalFn = watcher.fn;
watcher.fn = function(newVal, oldVal, scope) {
originalFn.call(this, newVal, oldVal, scope);
arrayRemove(array, watcher);
};
}

if (!array) {
array = scope.$$watchers = [];
}
Expand Down Expand Up @@ -391,37 +399,24 @@ function $RootScopeProvider(){
var deregisterFns = [];
var changeCount = 0;
var self = this;
var unwatchFlags = new Array(watchExpressions.length);
var unwatchCount = watchExpressions.length;

forEach(watchExpressions, function (expr, i) {
var exprFn = $parse(expr);
deregisterFns.push(self.$watch(exprFn, function (value, oldValue) {
deregisterFns.push(self.$watch(expr, function (value, oldValue) {
newValues[i] = value;
oldValues[i] = oldValue;
changeCount++;
if (unwatchFlags[i] && !exprFn.$$unwatch) unwatchCount++;
if (!unwatchFlags[i] && exprFn.$$unwatch) unwatchCount--;
unwatchFlags[i] = exprFn.$$unwatch;
}));
}, this);

deregisterFns.push(self.$watch(watchGroupFn, function () {
deregisterFns.push(self.$watch(function () {return changeCount;}, function () {
listener(newValues, oldValues, self);
if (unwatchCount === 0) {
watchGroupFn.$$unwatch = true;
} else {
watchGroupFn.$$unwatch = false;
}
}));

return function deregisterWatchGroup() {
forEach(deregisterFns, function (fn) {
fn();
});
};

function watchGroupFn() {return changeCount;}
},


Expand Down Expand Up @@ -566,7 +561,6 @@ function $RootScopeProvider(){
}
}
}
$watchCollectionWatch.$$unwatch = objGetter.$$unwatch;
return changeDetected;
}

Expand Down Expand Up @@ -662,7 +656,6 @@ function $RootScopeProvider(){
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
stableWatchesCandidates = [],
logIdx, logMsg, asyncTask;

beginPhase('$digest');
Expand Down Expand Up @@ -713,7 +706,6 @@ function $RootScopeProvider(){
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
watchLog[logIdx].push(logMsg);
}
if (watch.get.$$unwatch) stableWatchesCandidates.push({watch: watch, array: watchers});
} else if (watch === lastDirtyWatch) {
// If the most recently dirty watcher is now clean, short circuit since the remaining watchers
// have already been tested.
Expand Down Expand Up @@ -760,13 +752,6 @@ function $RootScopeProvider(){
$exceptionHandler(e);
}
}

for (length = stableWatchesCandidates.length - 1; length >= 0; --length) {
var candidate = stableWatchesCandidates[length];
if (candidate.watch.get.$$unwatch) {
arrayRemove(candidate.array, candidate.watch);
}
}
},


Expand Down
4 changes: 1 addition & 3 deletions src/ng/sce.js
Original file line number Diff line number Diff line change
Expand Up @@ -787,9 +787,7 @@ function $SceProvider() {
return parsed;
} else {
return function sceParseAsTrusted(self, locals) {
var result = sce.getTrusted(type, parsed(self, locals));
sceParseAsTrusted.$$unwatch = parsed.$$unwatch;
return result;
return sce.getTrusted(type, parsed(self, locals));
};
}
};
Expand Down
Loading

0 comments on commit be98f7c

Please sign in to comment.