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

perf($interpolate/$compile): port optimization from dart #6598

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -1803,9 +1803,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
bindings = parent.data('$binding') || [];
bindings.push(interpolateFn);
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
scope.$watchSet(interpolateFn.expressions, interpolateFn.$$invoke(function(value) {
node[0].nodeValue = value;
});
}));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just for clarifications:
We use interpolateFn.$$invoke(listener) so we just interpolate once and reuse the same instance for multiple invocations of link. This couldn't be done if we were to use dart's .setter property

})
});
}
Expand Down Expand Up @@ -1866,7 +1866,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attr[name] = interpolateFn(scope);
($$observers[name] || ($$observers[name] = [])).$$inter = true;
(attr.$$observers && attr.$$observers[name].$$scope || scope).
$watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
$watchSet(interpolateFn.expressions, interpolateFn.$$invoke(function (newValue, oldValue) {
//special case for class attribute addition + removal
//so that class changes can tap into the animation
//hooks provided by the $animate service. Be sure to
Expand All @@ -1878,7 +1878,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else {
attr.$set(name, newValue);
}
});
}));
}
};
}
Expand Down
91 changes: 55 additions & 36 deletions src/ng/interpolate.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,32 +125,33 @@ function $InterpolateProvider() {
var startIndex,
endIndex,
index = 0,
parts = [],
length = text.length,
separators = [],
expressions = [],
hasInterpolation = false,
hasText = false,
fn,
exp,
concat = [];

while(index < length) {
while(index < text.length) {
if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) &&
((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) {
(index != startIndex) && parts.push(text.substring(index, startIndex));
parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex)));
if (index !== startIndex) hasText = true;
separators.push(text.substring(index, startIndex));
expressions.push(fn = interpolateParse(exp = text.substring(startIndex + startSymbolLength, endIndex)));
fn.exp = exp;
index = endIndex + endSymbolLength;
hasInterpolation = true;
} else {
// we did not find anything, so we have to add the remainder to the parts array
(index != length) && parts.push(text.substring(index));
index = length;
// we did not find an interpolation, so we have to add the remainder to the separators array
if (index !== text.length) hasText = true;
separators.push(text.substring(index));
break;
}
}

if (!(length = parts.length)) {
// we added, nothing, must have been an empty string.
parts.push('');
length = 1;
if (separators.length === expressions.length) {
separators.push('');
}

// Concatenating expressions makes it hard to reason about whether some combination of
Expand All @@ -159,44 +160,62 @@ function $InterpolateProvider() {
// that's used is assigned or constructed by some JS code somewhere that is more testable or
// make it obvious that you bound the value to some user controlled value. This helps reduce
// the load when auditing for XSS issues.
if (trustedContext && parts.length > 1) {
if (trustedContext && hasInterpolation && (hasText || expressions.length > 1)) {
throw $interpolateMinErr('noconcat',
"Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
"interpolations that concatenate multiple expressions when a trusted value is " +
"required. See http://docs.angularjs.org/api/ng.$sce", text);
}

if (!mustHaveExpression || hasInterpolation) {
concat.length = length;
concat.length = separators.length + expressions.length;
var computeFn = function (values, context) {
for(var i = 0, ii = expressions.length; i<ii; i++) {
concat[2*i] = separators[i];
concat[(2*i)+1] = values ? values[i] : expressions[i](context);
}
concat[2*ii] = separators[ii];
return concat.join('');
};

fn = function(context) {
return computeFn(null, context);
};
fn.exp = text;
fn.$$invoke = function (listener) {
Copy link
Contributor

Choose a reason for hiding this comment

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

return function (values, oldValues, scope) {
var current = computeFn(values, scope);
listener(current, this.$$lastInter == null ? current : this.$$lastInter, scope);
this.$$lastInter = current;
};
};
fn.separators = separators;
fn.expressions = expressions;
return fn;
}

function interpolateParse(expression) {
var exp = $parse(expression);
return function (scope) {
try {
for(var i = 0, ii = length, part; i<ii; i++) {
if (typeof (part = parts[i]) == 'function') {
part = part(context);
if (trustedContext) {
part = $sce.getTrusted(trustedContext, part);
} else {
part = $sce.valueOf(part);
}
if (part === null || isUndefined(part)) {
part = '';
} else if (typeof part != 'string') {
part = toJson(part);
}
}
concat[i] = part;
var value = exp(scope);
if (trustedContext) {
value = $sce.getTrusted(trustedContext, value);
} else {
value = $sce.valueOf(value);
}
return concat.join('');
}
catch(err) {
if (value === null || isUndefined(value)) {
value = '';
} else if (typeof value != 'string') {
value = toJson(value);
}
return value;
} catch(err) {
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
err.toString());
err.toString());
$exceptionHandler(newErr);
}
};
fn.exp = text;
fn.parts = parts;
return fn;
}
}

Expand Down
55 changes: 55 additions & 0 deletions src/ng/rootScope.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,61 @@ function $RootScopeProvider(){
};
},

/**
* @ngdoc method
* @name $rootScope.Scope#$watchSet
* @function
*
* @description
* A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`.
* If any one expression in the collection changes the `listener` is executed.
*
* - The items in the `watchCollection` array are observed via standard $watch operation and are examined on every
* call to $digest() to see if any items changes.
* - The `listener` is called whenever any expression in the `watchExpressions` array changes.
*
* @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually
* watched using {@link ng.$rootScope.Scope#$watch $watch()}
*
* @param {function(newValues, oldValues, scope)} listener Callback called whenever the return value of any
* expression in `watchExpressions` changes
* The `newValues` array contains the current values of the `watchExpressions`, with the indexes matching
* those of `watchExpression`
* and the `oldValues` array contains the previous values of the `watchExpressions`, with the indexes matching
* those of `watchExpression`
* The `scope` refers to the current scope.
*
* @returns {function()} Returns a de-registration function for all listeners.
*/
$watchSet: function (watchExpressions, listener) {
if (watchExpressions.length === 0) return noop;

var self = this,
oldValues = new Array(watchExpressions.length),
newValues = new Array(watchExpressions.length);

var deregisterFns = [],
changeCount = 0;

forEach(watchExpressions, function (expr, i) {
deregisterFns.push(self.$watch(expr, function (value, oldValue) {
newValues[i] = value;
oldValues[i] = oldValue;
changeCount++;
}));
});

deregisterFns.push(this.$watch(function () {return changeCount;}, function () {
listener.call(this, newValues, oldValues, self);
}));

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


/**
* @ngdoc method
Expand Down
4 changes: 2 additions & 2 deletions src/ngScenario/Scenario.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ _jQuery.fn.bindings = function(windowJquery, bindExp) {
}
for(var fns, j=0, jj=binding.length; j<jj; j++) {
fns = binding[j];
if (fns.parts) {
fns = fns.parts;
if (fns.expressions) {
fns = fns.expressions;
Copy link
Contributor

Choose a reason for hiding this comment

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

@juliemr would this change also affect protractor? how do you introspect interpolations in protractor?

Copy link
Member

Choose a reason for hiding this comment

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

This should not require a change in Protractor. Protractor currently uses the element's data function and gets data('$binding').exp, which has not changed in this PR from what I see.

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks!

} else {
fns = [fns];
}
Expand Down
96 changes: 55 additions & 41 deletions test/ng/interpolateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,77 +123,84 @@ describe('$interpolate', function() {
}));

it('should not get confused with same markers', inject(function($interpolate) {
expect($interpolate('---').parts).toEqual(['---']);
expect($interpolate('---').separators).toEqual(['---']);
expect($interpolate('---').expressions).toEqual([]);
expect($interpolate('----')()).toEqual('');
expect($interpolate('--1--')()).toEqual('1');
}));
});


describe('parseBindings', function() {
it('should Parse Text With No Bindings', inject(function($interpolate) {
var parts = $interpolate("a").parts;
expect(parts.length).toEqual(1);
expect(parts[0]).toEqual("a");
expect($interpolate("a").separators).toEqual(['a']);
expect($interpolate("a").expressions).toEqual([]);
}));

it('should Parse Empty Text', inject(function($interpolate) {
var parts = $interpolate("").parts;
expect(parts.length).toEqual(1);
expect(parts[0]).toEqual("");
expect($interpolate("").separators).toEqual(['']);
expect($interpolate("").expressions).toEqual([]);
}));

it('should Parse Inner Binding', inject(function($interpolate) {
var parts = $interpolate("a{{b}}C").parts;
expect(parts.length).toEqual(3);
expect(parts[0]).toEqual("a");
expect(parts[1].exp).toEqual("b");
expect(parts[1]({b:123})).toEqual(123);
expect(parts[2]).toEqual("C");
var interpolateFn = $interpolate("a{{b}}C"),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['a', 'C']);
expect(expressions.length).toEqual(1);
expect(expressions[0].exp).toEqual('b');
expect(expressions[0]({b:123})).toEqual('123');
}));

it('should Parse Ending Binding', inject(function($interpolate) {
var parts = $interpolate("a{{b}}").parts;
expect(parts.length).toEqual(2);
expect(parts[0]).toEqual("a");
expect(parts[1].exp).toEqual("b");
expect(parts[1]({b:123})).toEqual(123);
var interpolateFn = $interpolate("a{{b}}"),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['a', '']);
expect(expressions.length).toEqual(1);
expect(expressions[0].exp).toEqual('b');
expect(expressions[0]({b:123})).toEqual('123');
}));

it('should Parse Begging Binding', inject(function($interpolate) {
var parts = $interpolate("{{b}}c").parts;
expect(parts.length).toEqual(2);
expect(parts[0].exp).toEqual("b");
expect(parts[1]).toEqual("c");
var interpolateFn = $interpolate("{{b}}c"),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['', 'c']);
expect(expressions.length).toEqual(1);
expect(expressions[0].exp).toEqual('b');
expect(expressions[0]({b:123})).toEqual('123');
}));

it('should Parse Loan Binding', inject(function($interpolate) {
var parts = $interpolate("{{b}}").parts;
expect(parts.length).toEqual(1);
expect(parts[0].exp).toEqual("b");
var interpolateFn = $interpolate("{{b}}"),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['', '']);
expect(expressions.length).toEqual(1);
expect(expressions[0].exp).toEqual('b');
expect(expressions[0]({b:123})).toEqual('123');
}));

it('should Parse Two Bindings', inject(function($interpolate) {
var parts = $interpolate("{{b}}{{c}}").parts;
expect(parts.length).toEqual(2);
expect(parts[0].exp).toEqual("b");
expect(parts[1].exp).toEqual("c");
var interpolateFn = $interpolate("{{b}}{{c}}"),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['', '', '']);
expect(expressions.length).toEqual(2);
expect(expressions[0].exp).toEqual('b');
expect(expressions[1].exp).toEqual('c');
}));

it('should Parse Two Bindings With Text In Middle', inject(function($interpolate) {
var parts = $interpolate("{{b}}x{{c}}").parts;
expect(parts.length).toEqual(3);
expect(parts[0].exp).toEqual("b");
expect(parts[1]).toEqual("x");
expect(parts[2].exp).toEqual("c");
var interpolateFn = $interpolate("{{b}}x{{c}}"),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['', 'x', '']);
expect(expressions.length).toEqual(2);
expect(expressions[0].exp).toEqual('b');
expect(expressions[1].exp).toEqual('c');
}));

it('should Parse Multiline', inject(function($interpolate) {
var parts = $interpolate('"X\nY{{A\n+B}}C\nD"').parts;
expect(parts.length).toEqual(3);
expect(parts[0]).toEqual('"X\nY');
expect(parts[1].exp).toEqual('A\n+B');
expect(parts[2]).toEqual('C\nD"');
var interpolateFn = $interpolate('"X\nY{{A\n+B}}C\nD"'),
separators = interpolateFn.separators, expressions = interpolateFn.expressions;
expect(separators).toEqual(['"X\nY', 'C\nD"']);
expect(expressions.length).toEqual(1);
expect(expressions[0].exp).toEqual('A\n+B');
}));
});

Expand All @@ -207,6 +214,12 @@ describe('$interpolate', function() {
"$interpolate", "noconcat", "Error while interpolating: constant/{{var}}\nStrict " +
"Contextual Escaping disallows interpolations that concatenate multiple expressions " +
"when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce");
expect(function() {
$interpolate('{{var}}/constant', true, isTrustedContext);
}).toThrowMinErr(
"$interpolate", "noconcat", "Error while interpolating: {{var}}/constant\nStrict " +
"Contextual Escaping disallows interpolations that concatenate multiple expressions " +
"when a trusted value is required. See http://docs.angularjs.org/api/ng.$sce");
expect(function() {
$interpolate('{{foo}}{{bar}}', true, isTrustedContext);
}).toThrowMinErr(
Expand Down Expand Up @@ -248,7 +261,8 @@ describe('$interpolate', function() {
});

inject(function($interpolate) {
expect($interpolate('---').parts).toEqual(['---']);
expect($interpolate('---').separators).toEqual(['---']);
expect($interpolate('---').expressions).toEqual([]);
expect($interpolate('----')()).toEqual('');
expect($interpolate('--1--')()).toEqual('1');
});
Expand Down
Loading