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

Commit 0f025c8

Browse files
committed
WIP: perf($parse): only execute watched expressions when the inputs change
1 parent a1a9585 commit 0f025c8

File tree

4 files changed

+145
-21
lines changed

4 files changed

+145
-21
lines changed

src/ng/compile.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -1699,7 +1699,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16991699
attrs[attrName], newIsolateScopeDirective.name);
17001700
};
17011701
lastValue = isolateBindingContext[scopeName] = parentGet(scope);
1702-
var unwatch = scope.$watch($parse(attrs[attrName], function parentValueWatch(parentValue) {
1702+
var parentValueWatch = function parentValueWatch(parentValue) {
17031703
if (!compare(parentValue, isolateBindingContext[scopeName])) {
17041704
// we are out of sync and need to copy
17051705
if (!compare(parentValue, lastValue)) {
@@ -1711,7 +1711,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17111711
}
17121712
}
17131713
return lastValue = parentValue;
1714-
}), null, parentGet.literal);
1714+
};
1715+
parentValueWatch.externalInput = true;
1716+
var unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
17151717
isolateScope.$on('$destroy', unwatch);
17161718
break;
17171719

src/ng/parse.js

+120-19
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,12 @@ Lexer.prototype = {
377377
};
378378

379379

380+
function isConstant(exp) {
381+
return exp.constant;
382+
}
383+
384+
385+
380386
/**
381387
* @constructor
382388
*/
@@ -494,23 +500,26 @@ Parser.prototype = {
494500
return extend(function(self, locals) {
495501
return fn(self, locals, right);
496502
}, {
497-
constant:right.constant
503+
constant:right.constant,
504+
inputs: [right]
498505
});
499506
},
500507

501508
ternaryFn: function(left, middle, right){
502509
return extend(function(self, locals){
503510
return left(self, locals) ? middle(self, locals) : right(self, locals);
504511
}, {
505-
constant: left.constant && middle.constant && right.constant
512+
constant: left.constant && middle.constant && right.constant,
513+
inputs: [left, middle, right]
506514
});
507515
},
508516

509517
binaryFn: function(left, fn, right) {
510518
return extend(function(self, locals) {
511519
return fn(self, locals, left, right);
512520
}, {
513-
constant:left.constant && right.constant
521+
constant:left.constant && right.constant,
522+
inputs: [left, right]
514523
});
515524
},
516525

@@ -558,7 +567,9 @@ Parser.prototype = {
558567
}
559568
}
560569

561-
return function $parseFilter(self, locals) {
570+
var inputs = !fn.externalInput && [inputFn].concat(argsFn || []);
571+
572+
return extend(function $parseFilter(self, locals) {
562573
var input = inputFn(self, locals);
563574
if (args) {
564575
args[0] = input;
@@ -572,7 +583,10 @@ Parser.prototype = {
572583
}
573584

574585
return fn(input);
575-
};
586+
}, {
587+
constant: inputs && inputs.every(isConstant),
588+
inputs: inputs
589+
});
576590
},
577591

578592
expression: function() {
@@ -589,9 +603,11 @@ Parser.prototype = {
589603
this.text.substring(0, token.index) + '] can not be assigned to', token);
590604
}
591605
right = this.ternary();
592-
return function $parseAssignment(scope, locals) {
606+
return extend(function $parseAssignment(scope, locals) {
593607
return left.assign(scope, right(scope, locals), locals);
594-
};
608+
}, {
609+
inputs: [left, right]
610+
});
595611
}
596612
return left;
597613
},
@@ -760,7 +776,6 @@ Parser.prototype = {
760776
// This is used with json array declaration
761777
arrayDeclaration: function () {
762778
var elementFns = [];
763-
var allConstant = true;
764779
if (this.peekToken().text !== ']') {
765780
do {
766781
if (this.peek(']')) {
@@ -769,9 +784,6 @@ Parser.prototype = {
769784
}
770785
var elementFn = this.expression();
771786
elementFns.push(elementFn);
772-
if (!elementFn.constant) {
773-
allConstant = false;
774-
}
775787
} while (this.expect(','));
776788
}
777789
this.consume(']');
@@ -784,13 +796,13 @@ Parser.prototype = {
784796
return array;
785797
}, {
786798
literal: true,
787-
constant: allConstant
799+
constant: elementFns.every(isConstant),
800+
inputs: elementFns
788801
});
789802
},
790803

791804
object: function () {
792805
var keys = [], values = [];
793-
var allConstant = true;
794806
if (this.peekToken().text !== '}') {
795807
do {
796808
if (this.peek('}')) {
@@ -802,9 +814,6 @@ Parser.prototype = {
802814
this.consume(':');
803815
var value = this.expression();
804816
values.push(value);
805-
if (!value.constant) {
806-
allConstant = false;
807-
}
808817
} while (this.expect(','));
809818
}
810819
this.consume('}');
@@ -817,7 +826,8 @@ Parser.prototype = {
817826
return object;
818827
}, {
819828
literal: true,
820-
constant: allConstant
829+
constant: values.every(isConstant),
830+
inputs: values
821831
});
822832
}
823833
};
@@ -1045,6 +1055,9 @@ function $ParseProvider() {
10451055
parsedExpression.$$watchDelegate = parsedExpression.literal ?
10461056
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
10471057
}
1058+
else if (parsedExpression.inputs) {
1059+
parsedExpression.$$watchDelegate = inputsWatchDelegate;
1060+
}
10481061

10491062
cache[cacheKey] = parsedExpression;
10501063
}
@@ -1058,6 +1071,87 @@ function $ParseProvider() {
10581071
}
10591072
};
10601073

1074+
function collectExpressionInputs(inputs, list) {
1075+
for (var i = 0, ii = inputs.length; i < ii; i++) {
1076+
var input = inputs[i];
1077+
if (!input.constant) {
1078+
if (input.inputs) {
1079+
collectExpressionInputs(input.inputs, list);
1080+
}
1081+
else if (-1 === list.indexOf(input)) {
1082+
list.push(input);
1083+
}
1084+
}
1085+
}
1086+
1087+
return list;
1088+
}
1089+
1090+
function simpleEquals(o1, o2) {
1091+
if (o1 == null || o2 == null) return o1 === o2; // null/undefined
1092+
1093+
if (typeof o1 === "object") {
1094+
//The same object is not supported because it may have been mutated
1095+
if (o1 === o2) return false;
1096+
1097+
if (typeof o2 !== "object") return false;
1098+
1099+
//Dates
1100+
if (isDate(o1) && isDate(o2)) {
1101+
o1 = o1.getTime();
1102+
o2 = o2.getTime();
1103+
1104+
//Fallthru to the primitive equality check
1105+
}
1106+
1107+
//Otherwise objects are not supported - recursing over arrays/object would be too expensive
1108+
else {
1109+
return false;
1110+
}
1111+
}
1112+
1113+
//Primitive or NaN
1114+
return o1 === o2 || (o1 !== o1 && o2 !== o2);
1115+
}
1116+
1117+
function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) {
1118+
var inputExpressions = parsedExpression.$$inputs ||
1119+
(parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, []));
1120+
1121+
var inputs = [simpleEquals/*=something that will never equal an evaluated input*/];
1122+
var lastResult;
1123+
1124+
if (1 === inputExpressions.length) {
1125+
inputs = inputs[0];
1126+
inputExpressions = inputExpressions[0];
1127+
return scope.$watch(function expressionInputWatch(scope) {
1128+
var newVal = inputExpressions(scope);
1129+
if (!simpleEquals(newVal, inputs)) {
1130+
lastResult = parsedExpression(scope);
1131+
inputs = newVal;
1132+
}
1133+
return lastResult;
1134+
}, listener, objectEquality);
1135+
}
1136+
1137+
return scope.$watch(function expressionInputsWatch(scope) {
1138+
var changed = false;
1139+
1140+
for (var i=0, ii=inputExpressions.length; i<ii; i++) {
1141+
var valI = inputExpressions[i](scope);
1142+
if (changed || (changed = !simpleEquals(valI, inputs[i]))) {
1143+
inputs[i] = valI;
1144+
}
1145+
}
1146+
1147+
if (changed) {
1148+
lastResult = parsedExpression(scope);
1149+
}
1150+
1151+
return lastResult;
1152+
}, listener, objectEquality);
1153+
}
1154+
10611155
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
10621156
var unwatch, lastValue;
10631157
return unwatch = scope.$watch(function oneTimeWatch(scope) {
@@ -1116,14 +1210,21 @@ function $ParseProvider() {
11161210
function addInterceptor(parsedExpression, interceptorFn) {
11171211
if (!interceptorFn) return parsedExpression;
11181212

1119-
var fn = function interceptedExpression(scope, locals) {
1213+
var fn = function(scope, locals) {
11201214
var value = parsedExpression(scope, locals);
11211215
var result = interceptorFn(value, scope, locals);
11221216
// we only return the interceptor's result if the
11231217
// initial value is defined (for bind-once)
11241218
return isDefined(value) ? result : value;
11251219
};
1126-
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
1220+
1221+
if (parsedExpression.$$watchDelegate) {
1222+
//Only transfer the inputsWatchDelegate for interceptors not marked as having externalInput
1223+
if (!(parsedExpression.$$watchDelegate === inputsWatchDelegate && interceptorFn.externalInput)) {
1224+
fn.inputs = parsedExpression.inputs;
1225+
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
1226+
}
1227+
}
11271228
return fn;
11281229
}
11291230
}];

src/ng/rootScope.js

+1
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ function $RootScopeProvider(){
532532
var initRun = true;
533533
var oldLength = 0;
534534

535+
$watchCollectionInterceptor.externalInput = true;
535536
function $watchCollectionInterceptor(_value) {
536537
newValue = _value;
537538
var newLength, key, bothNaN, newItem, oldItem;

test/ng/parseSpec.js

+20
Original file line numberDiff line numberDiff line change
@@ -1281,6 +1281,26 @@ describe('parser', function() {
12811281
expect(log.empty()).toEqual([]);
12821282
}));
12831283

1284+
it('should calculate the literal every single time', inject(function($parse) {
1285+
var filterCalls = 0;
1286+
$filterProvider.register('foo', valueFn(function(input) {
1287+
filterCalls++;
1288+
return input;
1289+
}));
1290+
1291+
var watcherCalls = 0;
1292+
scope.$watch($parse('{x: 1} | foo'), function(input) {
1293+
expect(input).toEqual({x:1});
1294+
watcherCalls++;
1295+
});
1296+
scope.$digest();
1297+
expect(filterCalls).toBe(1);
1298+
expect(watcherCalls).toBe(1);
1299+
scope.$digest();
1300+
expect(filterCalls).toBe(1);
1301+
expect(watcherCalls).toBe(1);
1302+
}));
1303+
12841304
it('should only become stable when all the elements of an array have defined values', inject(function ($parse, $rootScope, log){
12851305
var fn = $parse('::[foo,bar]');
12861306
$rootScope.$watch(fn, function(value) { log(value); }, true);

0 commit comments

Comments
 (0)