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

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit fca6be7

Browse files
jbedardIgorMinar
authored andcommitted
perf($parse): execute watched expressions only when the inputs change
With this change, expressions like "firstName + ' ' + lastName | uppercase" will be analyzed and only the inputs for the expression will be watched (in this case "firstName" and "lastName"). Only when at least one of the inputs change, the expression will be evaluated. This change speeds up simple expressions like `firstName | noop` by ~15% and more complex expressions like `startDate | date` by ~2500%. BREAKING CHANGE: all filters are assumed to be stateless functions Previously it was a good practice to make all filters stateless, but now it's a requirement in order for the model change-observation to pick up all changes. If an existing filter is statefull, it can be flagged as such but keep in mind that this will result in a significant performance-penalty (or rather lost opportunity to benefit from a major perf improvement) that will affect the $digest duration. To flag a filter as stateful do the following: myApp.filter('myFilter', function() { function myFilter(input) { ... }; myFilter.$stateful = true; return myFilter; }); Closes#9006Closes#9082
1 parent ec9c0d7 commit fca6be7

6 files changed

Lines changed: 386 additions & 24 deletions

File tree

‎benchmarks/parsed-expressions-bp/main.html‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@
3131
<labelfor="operators">Binary/Unary operators</label>
3232
</li>
3333

34+
<li>
35+
<inputtype="radio" ng-model="expressionType" value="shortCircuitingOperators" id="shortCircuitingOperators">
36+
<labelfor="shortCircuitingOperators">AND/OR short-circuiting operators</label>
37+
</li>
38+
3439
<li>
3540
<inputtype="radio" ng-model="expressionType" value="filters" id="filters">
3641
<labelfor="filters">Filters</label>
@@ -134,6 +139,17 @@
134139
<spanbm-pe-watch="-rowIdx * 2 * rowIdx + rowIdx / rowIdx + 1"></span>
135140
</li>
136141

142+
<ling-switch-when="shortCircuitingOperators" ng-repeat="(rowIdx, row) in ::data">
143+
<spanbm-pe-watch="rowIdx && row.odd"></span>
144+
<spanbm-pe-watch="row.odd && row.even"></span>
145+
<spanbm-pe-watch="row.odd && !row.even"></span>
146+
<spanbm-pe-watch="row.odd || row.even"></span>
147+
<spanbm-pe-watch="row.odd || row.even || row.index"></span>
148+
<spanbm-pe-watch="row.index === 1 || row.index === 2"></span>
149+
<spanbm-pe-watch="row.num0 < row.num1 && row.num1 < row.num2"></span>
150+
<spanbm-pe-watch="row.num0 < row.num1 || row.num1 < row.num2"></span>
151+
</li>
152+
137153
<ling-switch-when="filters" ng-repeat="(rowIdx, row) in ::data">
138154
<spanbm-pe-watch="rowIdx | noop"></span>
139155
<spanbm-pe-watch="rowIdx | noop"></span>

‎src/ng/compile.js‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,7 +1725,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17251725
attrs[attrName],newIsolateScopeDirective.name);
17261726
};
17271727
lastValue=isolateBindingContext[scopeName]=parentGet(scope);
1728-
varunwatch=scope.$watch($parse(attrs[attrName],functionparentValueWatch(parentValue){
1728+
varparentValueWatch=functionparentValueWatch(parentValue){
17291729
if(!compare(parentValue,isolateBindingContext[scopeName])){
17301730
// we are out of sync and need to copy
17311731
if(!compare(parentValue,lastValue)){
@@ -1737,7 +1737,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17371737
}
17381738
}
17391739
returnlastValue=parentValue;
1740-
}),null,parentGet.literal);
1740+
};
1741+
parentValueWatch.$stateful=true;
1742+
varunwatch=scope.$watch($parse(attrs[attrName],parentValueWatch),null,parentGet.literal);
17411743
isolateScope.$on('$destroy',unwatch);
17421744
break;
17431745

‎src/ng/parse.js‎

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ Lexer.prototype = {
376376
};
377377

378378

379+
functionisConstant(exp){
380+
returnexp.constant;
381+
}
382+
379383
/**
380384
* @constructor
381385
*/
@@ -493,7 +497,8 @@ Parser.prototype = {
493497
returnextend(function(self,locals){
494498
returnfn(self,locals,right);
495499
},{
496-
constant:right.constant
500+
constant:right.constant,
501+
inputs: [right]
497502
});
498503
},
499504

@@ -505,11 +510,12 @@ Parser.prototype = {
505510
});
506511
},
507512

508-
binaryFn: function(left,fn,right){
513+
binaryFn: function(left,fn,right,isBranching){
509514
returnextend(function(self,locals){
510515
returnfn(self,locals,left,right);
511516
},{
512-
constant:left.constant&&right.constant
517+
constant: left.constant&&right.constant,
518+
inputs: !isBranching&&[left,right]
513519
});
514520
},
515521

@@ -557,7 +563,9 @@ Parser.prototype = {
557563
}
558564
}
559565

560-
returnfunction$parseFilter(self,locals){
566+
varinputs=[inputFn].concat(argsFn||[]);
567+
568+
returnextend(function$parseFilter(self,locals){
561569
varinput=inputFn(self,locals);
562570
if(args){
563571
args[0]=input;
@@ -571,7 +579,10 @@ Parser.prototype = {
571579
}
572580

573581
returnfn(input);
574-
};
582+
},{
583+
constant: !fn.$stateful&&inputs.every(isConstant),
584+
inputs: !fn.$stateful&&inputs
585+
});
575586
},
576587

577588
expression: function(){
@@ -588,9 +599,11 @@ Parser.prototype = {
588599
this.text.substring(0,token.index)+'] can not be assigned to',token);
589600
}
590601
right=this.ternary();
591-
returnfunction$parseAssignment(scope,locals){
602+
returnextend(function$parseAssignment(scope,locals){
592603
returnleft.assign(scope,right(scope,locals),locals);
593-
};
604+
},{
605+
inputs: [left,right]
606+
});
594607
}
595608
returnleft;
596609
},
@@ -615,7 +628,7 @@ Parser.prototype = {
615628
varleft=this.logicalAND();
616629
vartoken;
617630
while((token=this.expect('||'))){
618-
left=this.binaryFn(left,token.fn,this.logicalAND());
631+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
619632
}
620633
returnleft;
621634
},
@@ -624,7 +637,7 @@ Parser.prototype = {
624637
varleft=this.equality();
625638
vartoken;
626639
if((token=this.expect('&&'))){
627-
left=this.binaryFn(left,token.fn,this.logicalAND());
640+
left=this.binaryFn(left,token.fn,this.logicalAND(),true);
628641
}
629642
returnleft;
630643
},
@@ -759,7 +772,6 @@ Parser.prototype = {
759772
// This is used with json array declaration
760773
arrayDeclaration: function(){
761774
varelementFns=[];
762-
varallConstant=true;
763775
if(this.peekToken().text!==']'){
764776
do{
765777
if(this.peek(']')){
@@ -768,9 +780,6 @@ Parser.prototype = {
768780
}
769781
varelementFn=this.expression();
770782
elementFns.push(elementFn);
771-
if(!elementFn.constant){
772-
allConstant=false;
773-
}
774783
}while(this.expect(','));
775784
}
776785
this.consume(']');
@@ -783,13 +792,13 @@ Parser.prototype = {
783792
returnarray;
784793
},{
785794
literal: true,
786-
constant: allConstant
795+
constant: elementFns.every(isConstant),
796+
inputs: elementFns
787797
});
788798
},
789799

790800
object: function(){
791801
varkeys=[],valueFns=[];
792-
varallConstant=true;
793802
if(this.peekToken().text!=='}'){
794803
do{
795804
if(this.peek('}')){
@@ -801,9 +810,6 @@ Parser.prototype = {
801810
this.consume(':');
802811
varvalue=this.expression();
803812
valueFns.push(value);
804-
if(!value.constant){
805-
allConstant=false;
806-
}
807813
}while(this.expect(','));
808814
}
809815
this.consume('}');
@@ -816,7 +822,8 @@ Parser.prototype = {
816822
returnobject;
817823
},{
818824
literal: true,
819-
constant: allConstant
825+
constant: valueFns.every(isConstant),
826+
inputs: valueFns
820827
});
821828
}
822829
};
@@ -1043,6 +1050,8 @@ function $ParseProvider() {
10431050
parsedExpression=wrapSharedExpression(parsedExpression);
10441051
parsedExpression.$$watchDelegate=parsedExpression.literal ?
10451052
oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
1053+
}elseif(parsedExpression.inputs){
1054+
parsedExpression.$$watchDelegate=inputsWatchDelegate;
10461055
}
10471056

10481057
cache[cacheKey]=parsedExpression;
@@ -1057,6 +1066,88 @@ function $ParseProvider() {
10571066
}
10581067
};
10591068

1069+
functioncollectExpressionInputs(inputs,list){
1070+
for(vari=0,ii=inputs.length;i<ii;i++){
1071+
varinput=inputs[i];
1072+
if(!input.constant){
1073+
if(input.inputs){
1074+
collectExpressionInputs(input.inputs,list);
1075+
}elseif(list.indexOf(input)===-1){// TODO(perf) can we do better?
1076+
list.push(input);
1077+
}
1078+
}
1079+
}
1080+
1081+
returnlist;
1082+
}
1083+
1084+
functionexpressionInputDirtyCheck(newValue,oldValueOfValue){
1085+
1086+
if(newValue==null||oldValueOfValue==null){// null/undefined
1087+
returnnewValue===oldValueOfValue;
1088+
}
1089+
1090+
if(typeofnewValue==='object'){
1091+
1092+
// attempt to convert the value to a primitive type
1093+
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
1094+
// be cheaply dirty-checked
1095+
newValue=newValue.valueOf();
1096+
1097+
if(typeofnewValue==='object'){
1098+
// objects/arrays are not supported - deep-watching them would be too expensive
1099+
returnfalse;
1100+
}
1101+
1102+
// fall-through to the primitive equality check
1103+
}
1104+
1105+
//Primitive or NaN
1106+
returnnewValue===oldValueOfValue||(newValue!==newValue&&oldValueOfValue!==oldValueOfValue);
1107+
}
1108+
1109+
functioninputsWatchDelegate(scope,listener,objectEquality,parsedExpression){
1110+
varinputExpressions=parsedExpression.$$inputs||
1111+
(parsedExpression.$$inputs=collectExpressionInputs(parsedExpression.inputs,[]));
1112+
1113+
varlastResult;
1114+
1115+
if(inputExpressions.length===1){
1116+
varoldInputValue=expressionInputDirtyCheck;// init to something unique so that equals check fails
1117+
inputExpressions=inputExpressions[0];
1118+
returnscope.$watch(functionexpressionInputWatch(scope){
1119+
varnewInputValue=inputExpressions(scope);
1120+
if(!expressionInputDirtyCheck(newInputValue,oldInputValue)){
1121+
lastResult=parsedExpression(scope);
1122+
oldInputValue=newInputValue&&newInputValue.valueOf();
1123+
}
1124+
returnlastResult;
1125+
},listener,objectEquality);
1126+
}
1127+
1128+
varoldInputValueOfValues=[];
1129+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1130+
oldInputValueOfValues[i]=expressionInputDirtyCheck;// init to something unique so that equals check fails
1131+
}
1132+
1133+
returnscope.$watch(functionexpressionInputsWatch(scope){
1134+
varchanged=false;
1135+
1136+
for(vari=0,ii=inputExpressions.length;i<ii;i++){
1137+
varnewInputValue=inputExpressions[i](scope);
1138+
if(changed||(changed=!expressionInputDirtyCheck(newInputValue,oldInputValueOfValues[i]))){
1139+
oldInputValueOfValues[i]=newInputValue&&newInputValue.valueOf();
1140+
}
1141+
}
1142+
1143+
if(changed){
1144+
lastResult=parsedExpression(scope);
1145+
}
1146+
1147+
returnlastResult;
1148+
},listener,objectEquality);
1149+
}
1150+
10601151
functiononeTimeWatchDelegate(scope,listener,objectEquality,parsedExpression){
10611152
varunwatch,lastValue;
10621153
returnunwatch=scope.$watch(functiononeTimeWatch(scope){
@@ -1122,7 +1213,18 @@ function $ParseProvider() {
11221213
// initial value is defined (for bind-once)
11231214
returnisDefined(value) ? result : value;
11241215
};
1125-
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1216+
1217+
// Propagate $$watchDelegates other then inputsWatchDelegate
1218+
if(parsedExpression.$$watchDelegate&&
1219+
parsedExpression.$$watchDelegate!==inputsWatchDelegate){
1220+
fn.$$watchDelegate=parsedExpression.$$watchDelegate;
1221+
}elseif(!interceptorFn.$stateful){
1222+
// If there is an interceptor, but no watchDelegate then treat the interceptor like
1223+
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
1224+
fn.$$watchDelegate=inputsWatchDelegate;
1225+
fn.inputs=[parsedExpression];
1226+
}
1227+
11261228
returnfn;
11271229
}
11281230
}];

‎src/ng/rootScope.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,8 @@ function $RootScopeProvider(){
515515
* de-registration function is executed, the internal watch operation is terminated.
516516
*/
517517
$watchCollection: function(obj,listener){
518+
$watchCollectionInterceptor.$stateful=true;
519+
518520
varself=this;
519521
// the current value, updated on each dirty-check run
520522
varnewValue;

‎test/ng/directive/ngRepeatSpec.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,12 +867,15 @@ describe('ngRepeat', function() {
867867
// This creates one item, but it has no parent so we can't get to it
868868
$rootScope.items=[1,2];
869869
$rootScope.$apply();
870+
expect(logs).toContain(1);
871+
expect(logs).toContain(2);
872+
logs.length=0;
870873

871874
// This cleans up to prevent memory leak
872875
$rootScope.items=[];
873876
$rootScope.$apply();
874877
expect(angular.mock.dump(element)).toBe('<!-- ngRepeat: i in items -->');
875-
expect(logs).toEqual([1,2,1,2]);
878+
expect(logs.length).toBe(0);
876879
}));
877880

878881

@@ -894,12 +897,15 @@ describe('ngRepeat', function() {
894897
// This creates one item, but it has no parent so we can't get to it
895898
$rootScope.items=[1,2];
896899
$rootScope.$apply();
900+
expect(logs).toContain(1);
901+
expect(logs).toContain(2);
902+
logs.length=0;
897903

898904
// This cleans up to prevent memory leak
899905
$rootScope.items=[];
900906
$rootScope.$apply();
901907
expect(sortedHtml(element)).toBe('<span>-</span><!-- ngRepeat: i in items --><span>-</span>');
902-
expect(logs).toEqual([1,2,1,2]);
908+
expect(logs.length).toBe(0);
903909
}));
904910

905911

0 commit comments

Comments
 (0)