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

Commit e656a78

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

File tree

4 files changed

+142
-20
lines changed

4 files changed

+142
-20
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

+117-18
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ Lexer.prototype = {
377377
};
378378

379379

380+
function isConstant(exp) {
381+
return exp.constant;
382+
}
383+
380384
/**
381385
* @constructor
382386
*/
@@ -494,23 +498,26 @@ Parser.prototype = {
494498
return extend(function(self, locals) {
495499
return fn(self, locals, right);
496500
}, {
497-
constant:right.constant
501+
constant:right.constant,
502+
inputs: [right]
498503
});
499504
},
500505

501506
ternaryFn: function(left, middle, right){
502507
return extend(function(self, locals){
503508
return left(self, locals) ? middle(self, locals) : right(self, locals);
504509
}, {
505-
constant: left.constant && middle.constant && right.constant
510+
constant: left.constant && middle.constant && right.constant,
511+
inputs: [left, middle, right]
506512
});
507513
},
508514

509515
binaryFn: function(left, fn, right) {
510516
return extend(function(self, locals) {
511517
return fn(self, locals, left, right);
512518
}, {
513-
constant:left.constant && right.constant
519+
constant:left.constant && right.constant,
520+
inputs: [left, right]
514521
});
515522
},
516523

@@ -558,7 +565,9 @@ Parser.prototype = {
558565
}
559566
}
560567

561-
return function $parseFilter(self, locals) {
568+
var inputs = !fn.externalInput && [inputFn].concat(argsFn || []);
569+
570+
return extend(function $parseFilter(self, locals) {
562571
var input = inputFn(self, locals);
563572
if (args) {
564573
args[0] = input;
@@ -572,7 +581,10 @@ Parser.prototype = {
572581
}
573582

574583
return fn(input);
575-
};
584+
}, {
585+
constant: inputs && inputs.every(isConstant),
586+
inputs: inputs
587+
});
576588
},
577589

578590
expression: function() {
@@ -589,9 +601,11 @@ Parser.prototype = {
589601
this.text.substring(0, token.index) + '] can not be assigned to', token);
590602
}
591603
right = this.ternary();
592-
return function $parseAssignment(scope, locals) {
604+
return extend(function $parseAssignment(scope, locals) {
593605
return left.assign(scope, right(scope, locals), locals);
594-
};
606+
}, {
607+
inputs: [left, right]
608+
});
595609
}
596610
return left;
597611
},
@@ -760,7 +774,6 @@ Parser.prototype = {
760774
// This is used with json array declaration
761775
arrayDeclaration: function () {
762776
var elementFns = [];
763-
var allConstant = true;
764777
if (this.peekToken().text !== ']') {
765778
do {
766779
if (this.peek(']')) {
@@ -769,9 +782,6 @@ Parser.prototype = {
769782
}
770783
var elementFn = this.expression();
771784
elementFns.push(elementFn);
772-
if (!elementFn.constant) {
773-
allConstant = false;
774-
}
775785
} while (this.expect(','));
776786
}
777787
this.consume(']');
@@ -784,13 +794,13 @@ Parser.prototype = {
784794
return array;
785795
}, {
786796
literal: true,
787-
constant: allConstant
797+
constant: elementFns.every(isConstant),
798+
inputs: elementFns
788799
});
789800
},
790801

791802
object: function () {
792803
var keys = [], values = [];
793-
var allConstant = true;
794804
if (this.peekToken().text !== '}') {
795805
do {
796806
if (this.peek('}')) {
@@ -802,9 +812,6 @@ Parser.prototype = {
802812
this.consume(':');
803813
var value = this.expression();
804814
values.push(value);
805-
if (!value.constant) {
806-
allConstant = false;
807-
}
808815
} while (this.expect(','));
809816
}
810817
this.consume('}');
@@ -817,7 +824,8 @@ Parser.prototype = {
817824
return object;
818825
}, {
819826
literal: true,
820-
constant: allConstant
827+
constant: values.every(isConstant),
828+
inputs: values
821829
});
822830
}
823831
};
@@ -1045,6 +1053,9 @@ function $ParseProvider() {
10451053
parsedExpression.$$watchDelegate = parsedExpression.literal ?
10461054
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
10471055
}
1056+
else if (parsedExpression.inputs) {
1057+
parsedExpression.$$watchDelegate = inputsWatchDelegate;
1058+
}
10481059

10491060
cache[cacheKey] = parsedExpression;
10501061
}
@@ -1058,6 +1069,87 @@ function $ParseProvider() {
10581069
}
10591070
};
10601071

1072+
function collectExpressionInputs(inputs, list) {
1073+
for (var i = 0, ii = inputs.length; i < ii; i++) {
1074+
var input = inputs[i];
1075+
if (!input.constant) {
1076+
if (input.inputs) {
1077+
collectExpressionInputs(input.inputs, list);
1078+
}
1079+
else if (-1 === list.indexOf(input)) {
1080+
list.push(input);
1081+
}
1082+
}
1083+
}
1084+
1085+
return list;
1086+
}
1087+
1088+
function simpleEquals(o1, o2) {
1089+
if (o1 == null || o2 == null) return o1 === o2; // null/undefined
1090+
1091+
if (typeof o1 === "object") {
1092+
//The same object is not supported because it may have been mutated
1093+
if (o1 === o2) return false;
1094+
1095+
if (typeof o2 !== "object") return false;
1096+
1097+
//Dates
1098+
if (isDate(o1) && isDate(o2)) {
1099+
o1 = o1.getTime();
1100+
o2 = o2.getTime();
1101+
1102+
//Fallthru to the primitive equality check
1103+
}
1104+
1105+
//Otherwise objects are not supported - recursing over arrays/object would be too expensive
1106+
else {
1107+
return false;
1108+
}
1109+
}
1110+
1111+
//Primitive or NaN
1112+
return o1 === o2 || (o1 !== o1 && o2 !== o2);
1113+
}
1114+
1115+
function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) {
1116+
var inputExpressions = parsedExpression.$$inputs ||
1117+
(parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, []));
1118+
1119+
var inputs = [simpleEquals/*=something that will never equal an evaluated input*/];
1120+
var lastResult;
1121+
1122+
if (1 === inputExpressions.length) {
1123+
inputs = inputs[0];
1124+
inputExpressions = inputExpressions[0];
1125+
return scope.$watch(function expressionInputWatch(scope) {
1126+
var newVal = inputExpressions(scope);
1127+
if (!simpleEquals(newVal, inputs)) {
1128+
lastResult = parsedExpression(scope);
1129+
inputs = newVal;
1130+
}
1131+
return lastResult;
1132+
}, listener, objectEquality);
1133+
}
1134+
1135+
return scope.$watch(function expressionInputsWatch(scope) {
1136+
var changed = false;
1137+
1138+
for (var i=0, ii=inputExpressions.length; i<ii; i++) {
1139+
var valI = inputExpressions[i](scope);
1140+
if (changed || (changed = !simpleEquals(valI, inputs[i]))) {
1141+
inputs[i] = valI;
1142+
}
1143+
}
1144+
1145+
if (changed) {
1146+
lastResult = parsedExpression(scope);
1147+
}
1148+
1149+
return lastResult;
1150+
}, listener, objectEquality);
1151+
}
1152+
10611153
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
10621154
var unwatch, lastValue;
10631155
return unwatch = scope.$watch(function oneTimeWatch(scope) {
@@ -1123,7 +1215,14 @@ function $ParseProvider() {
11231215
// initial value is defined (for bind-once)
11241216
return isDefined(value) ? result : value;
11251217
};
1126-
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
1218+
1219+
if (parsedExpression.$$watchDelegate) {
1220+
//Only transfer the inputsWatchDelegate for interceptors not marked as having externalInput
1221+
if (!(parsedExpression.$$watchDelegate === inputsWatchDelegate && interceptorFn.externalInput)) {
1222+
fn.inputs = parsedExpression.inputs;
1223+
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
1224+
}
1225+
}
11271226
return fn;
11281227
}
11291228
}];

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)