Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

WIP - only watch the inputs of parsed expressions #9006

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions benchmarks/parsed-expressions-bp/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
<label for="operators">Binary/Unary operators</label>
</li>

<li>
<input type="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
<label for="shortCircuitingOperators">AND/OR short-circuiting operators</label>
</li>

<li>
<input type="radio" ng-model="expressionType" value="filters" id="filters">
<label for="filters">Filters</label>
Expand Down Expand Up @@ -134,6 +139,17 @@
<span bm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
</li>

<li ng-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
<span bm-pe-watch="rowIdx && row.odd"></span>
<span bm-pe-watch="row.odd && row.even"></span>
<span bm-pe-watch="row.odd && !row.even"></span>
<span bm-pe-watch="row.odd || row.even"></span>
<span bm-pe-watch="row.odd || row.even || row.index"></span>
<span bm-pe-watch="row.index === 1 || row.index === 2"></span>
<span bm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
<span bm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
</li>

<li ng-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
<span bm-pe-watch="rowIdx | noop"></span>
<span bm-pe-watch="rowIdx | noop"></span>
Expand Down
6 changes: 4 additions & 2 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1699,7 +1699,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attrs[attrName], newIsolateScopeDirective.name);
};
lastValue = isolateBindingContext[scopeName] = parentGet(scope);
var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) {
var parentValueWatch = function parentValueWatch(parentValue) {
if (!compare(parentValue, isolateBindingContext[scopeName])) {
// we are out of sync and need to copy
if (!compare(parentValue, lastValue)) {
Expand All @@ -1711,7 +1711,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
return lastValue = parentValue;
}), null, parentGet.literal);
};
parentValueWatch.externalInput = true;
var unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
isolateScope.$on('$destroy', unwatch);
break;

Expand Down
161 changes: 129 additions & 32 deletions src/ng/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ var OPERATORS = extend(createMap(), {
'/':function(self, locals, a,b){return a(self, locals)/b(self, locals);},
'%':function(self, locals, a,b){return a(self, locals)%b(self, locals);},
'^':function(self, locals, a,b){return a(self, locals)^b(self, locals);},
'=':noop,
'===':function(self, locals, a, b){return a(self, locals)===b(self, locals);},
'!==':function(self, locals, a, b){return a(self, locals)!==b(self, locals);},
'==':function(self, locals, a,b){return a(self, locals)==b(self, locals);},
Expand All @@ -125,8 +124,11 @@ var OPERATORS = extend(createMap(), {
'||':function(self, locals, a,b){return a(self, locals)||b(self, locals);},
'&':function(self, locals, a,b){return a(self, locals)&b(self, locals);},
// '|':function(self, locals, a,b){return a|b;},
'|':function(self, locals, a,b){return b(self, locals)(self, locals, a(self, locals));},
'!':function(self, locals, a){return !a(self, locals);}
'!':function(self, locals, a){return !a(self, locals);},

//Tokenized as operators but parsed as assignment/filters
'=':true,
'|':true
});
/* jshint bitwise: true */
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
Expand Down Expand Up @@ -375,6 +377,10 @@ Lexer.prototype = {
};


function isConstant(exp) {
return exp.constant;
}

/**
* @constructor
*/
Expand Down Expand Up @@ -492,23 +498,25 @@ Parser.prototype = {
return extend(function(self, locals) {
return fn(self, locals, right);
}, {
constant:right.constant
constant:right.constant,
inputs: [right]
});
},

ternaryFn: function(left, middle, right){
return extend(function(self, locals){
return left(self, locals) ? middle(self, locals) : right(self, locals);
}, {
constant: left.constant && middle.constant && right.constant
constant:left.constant && middle.constant && right.constant
});
},

binaryFn: function(left, fn, right) {
binaryFn: function(left, fn, right, isBranching) {
return extend(function(self, locals) {
return fn(self, locals, left, right);
}, {
constant:left.constant && right.constant
constant: left.constant && right.constant,
inputs: !isBranching && [left, right]
});
},

Expand Down Expand Up @@ -537,12 +545,12 @@ Parser.prototype = {
var left = this.expression();
var token;
while ((token = this.expect('|'))) {
left = this.binaryFn(left, token.fn, this.filter());
left = this.filter(left);
}
return left;
},

filter: function() {
filter: function(inputFn) {
var token = this.expect();
var fn = this.$filter(token.text);
var argsFn;
Expand All @@ -556,7 +564,10 @@ Parser.prototype = {
}
}

return valueFn(function $parseFilter(self, locals, input) {
var inputs = [inputFn].concat(argsFn || []);

return extend(function $parseFilter(self, locals) {
var input = inputFn(self, locals);
if (args) {
args[0] = input;

Expand All @@ -569,6 +580,9 @@ Parser.prototype = {
}

return fn(input);
}, {
constant: !fn.externalInput && inputs.every(isConstant),
inputs: !fn.externalInput && inputs
});
},

Expand All @@ -586,9 +600,11 @@ Parser.prototype = {
this.text.substring(0, token.index) + '] can not be assigned to', token);
}
right = this.ternary();
return function $parseAssignment(scope, locals) {
return extend(function $parseAssignment(scope, locals) {
return left.assign(scope, right(scope, locals), locals);
};
}, {
inputs: [left, right]
});
}
return left;
},
Expand All @@ -613,7 +629,7 @@ Parser.prototype = {
var left = this.logicalAND();
var token;
while ((token = this.expect('||'))) {
left = this.binaryFn(left, token.fn, this.logicalAND());
left = this.binaryFn(left, token.fn, this.logicalAND(), true);
}
return left;
},
Expand All @@ -622,7 +638,7 @@ Parser.prototype = {
var left = this.equality();
var token;
if ((token = this.expect('&&'))) {
left = this.binaryFn(left, token.fn, this.logicalAND());
left = this.binaryFn(left, token.fn, this.logicalAND(), true);
}
return left;
},
Expand Down Expand Up @@ -757,7 +773,6 @@ Parser.prototype = {
// This is used with json array declaration
arrayDeclaration: function () {
var elementFns = [];
var allConstant = true;
if (this.peekToken().text !== ']') {
do {
if (this.peek(']')) {
Expand All @@ -766,9 +781,6 @@ Parser.prototype = {
}
var elementFn = this.expression();
elementFns.push(elementFn);
if (!elementFn.constant) {
allConstant = false;
}
} while (this.expect(','));
}
this.consume(']');
Expand All @@ -781,41 +793,38 @@ Parser.prototype = {
return array;
}, {
literal: true,
constant: allConstant
constant: elementFns.every(isConstant),
inputs: elementFns
});
},

object: function () {
var keyValues = [];
var allConstant = true;
var keys = [], values = [];
if (this.peekToken().text !== '}') {
do {
if (this.peek('}')) {
// Support trailing commas per ES5.1.
break;
}
var token = this.expect(),
key = token.string || token.text;
var token = this.expect();
keys.push(token.string || token.text);
this.consume(':');
var value = this.expression();
keyValues.push({key: key, value: value});
if (!value.constant) {
allConstant = false;
}
values.push(value);
} while (this.expect(','));
}
this.consume('}');

return extend(function $parseObjectLiteral(self, locals) {
var object = {};
for (var i = 0, ii = keyValues.length; i < ii; i++) {
var keyValue = keyValues[i];
object[keyValue.key] = keyValue.value(self, locals);
for (var i = 0, ii = values.length; i < ii; i++) {
object[keys[i]] = values[i](self, locals);
}
return object;
}, {
literal: true,
constant: allConstant
constant: values.every(isConstant),
inputs: values
});
}
};
Expand Down Expand Up @@ -1043,6 +1052,9 @@ function $ParseProvider() {
parsedExpression.$$watchDelegate = parsedExpression.literal ?
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
}
else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}

cache[cacheKey] = parsedExpression;
}
Expand All @@ -1056,6 +1068,81 @@ function $ParseProvider() {
}
};

function collectExpressionInputs(inputs, list) {
for (var i = 0, ii = inputs.length; i < ii; i++) {
var input = inputs[i];
if (!input.constant) {
if (input.inputs) {
collectExpressionInputs(input.inputs, list);
}
else if (-1 === list.indexOf(input)) {
list.push(input);
}
}
}

return list;
}

function simpleEquals(o1, o2) {
if (o1 == null || o2 == null) return o1 === o2; // null/undefined

if (typeof o1 === "object") {
//The same object is not supported because it may have been mutated
if (o1 === o2) return false;

if (typeof o2 !== "object") return false;

//Convert to primitive if possible
o1 = o1.valueOf();
o2 = o2.valueOf();

//If the type became a non-object then we can use the primitive check below
if (typeof o1 === "object") return false;
}

//Primitive or NaN
return o1 === o2 || (o1 !== o1 && o2 !== o2);
}

function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var inputExpressions = parsedExpression.$$inputs ||
(parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, []));

var inputs = [simpleEquals/*=something that will never equal an evaluated input*/];
var lastResult;

if (1 === inputExpressions.length) {
inputs = inputs[0];
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newVal = inputExpressions(scope);
if (!simpleEquals(newVal, inputs)) {
lastResult = parsedExpression(scope);
inputs = newVal;
}
return lastResult;
}, listener, objectEquality);
}

return scope.$watch(function expressionInputsWatch(scope) {
var changed = false;

for (var i=0, ii=inputExpressions.length; i<ii; i++) {
var valI = inputExpressions[i](scope);
if (changed || (changed = !simpleEquals(valI, inputs[i]))) {
inputs[i] = valI;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking of putting a break here. This may cause a couple extra parseExpression calls the first few digests until all the inputs have been evaluated once, but this will reduce the overhead when inputs are non-primitive. Or only break if non-primitive (maybe simpleEquals returns null instead of true/false to indicate a non-primitive), this might actually be the best for both cases...

}
}

if (changed) {
lastResult = parsedExpression(scope);
}

return lastResult;
}, listener, objectEquality);
}

function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch, lastValue;
return unwatch = scope.$watch(function oneTimeWatch(scope) {
Expand Down Expand Up @@ -1121,7 +1208,17 @@ function $ParseProvider() {
// initial value is defined (for bind-once)
return isDefined(value) ? result : value;
};
fn.$$watchDelegate = parsedExpression.$$watchDelegate;

//Propogate $$watchDelegates other then inputsWatchDelegate
if (parsedExpression.$$watchDelegate && parsedExpression.$$watchDelegate !== inputsWatchDelegate) {
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
}
//Treat the interceptorFn similar to filters - it is assumed to be a pure function unless flagged
else if (!interceptorFn.externalInput) {
fn.$$watchDelegate = inputsWatchDelegate;
fn.inputs = [parsedExpression];
}

return fn;
}
}];
Expand Down
2 changes: 2 additions & 0 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,8 @@ function $RootScopeProvider(){
* de-registration function is executed, the internal watch operation is terminated.
*/
$watchCollection: function(obj, listener) {
$watchCollectionInterceptor.externalInput = true;

var self = this;
// the current value, updated on each dirty-check run
var newValue;
Expand Down
Loading