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

Commit 546cb42

Browse files
committed
perf($interpolate): speed up interpolation by recreating watchGroup approach
This change undoes the use of watchGroup by code that uses $interpolate, by moving the optimizations into the $interpolate itself. While this is not ideal, it means that we are making the existing api faster rather than require people to use $interpolate differently in order to benefit from the speed improvements.
1 parent 1db3b8c commit 546cb42

File tree

4 files changed

+115
-95
lines changed

4 files changed

+115
-95
lines changed

src/ng/compile.js

+7-14
Original file line numberDiff line numberDiff line change
@@ -1804,9 +1804,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18041804
bindings = parent.data('$binding') || [];
18051805
bindings.push(interpolateFn);
18061806
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
1807-
scope.$watchGroup(interpolateFn.expressions,
1808-
function textInterpolationWatchAction(newValues) {
1809-
node[0].nodeValue = interpolateFn.compute(newValues);
1807+
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
1808+
node[0].nodeValue = value;
18101809
});
18111810
})
18121811
});
@@ -1848,8 +1847,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18481847
return {
18491848
pre: function attrInterpolatePreLinkFn(scope, element, attr) {
18501849
var $$observers = (attr.$$observers || (attr.$$observers = {}));
1851-
var interpolationResult;
1852-
var lastInterpolationResult;
18531850

18541851
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
18551852
throw $compileMinErr('nodomevents',
@@ -1868,25 +1865,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
18681865
// initialize attr object so that it's ready in case we need the value for isolate
18691866
// scope initialization, otherwise the value would not be available from isolate
18701867
// directive's linking fn during linking phase
1871-
attr[name] = interpolationResult = interpolateFn(scope);
1868+
attr[name] = interpolateFn(scope);
18721869

18731870
($$observers[name] || ($$observers[name] = [])).$$inter = true;
18741871
(attr.$$observers && attr.$$observers[name].$$scope || scope).
1875-
$watchGroup(interpolateFn.expressions,
1876-
function interpolationWatchAction(newValues) {
1877-
1878-
lastInterpolationResult = interpolationResult;
1879-
interpolationResult = interpolateFn.compute(newValues);
1872+
$watch(interpolateFn, function interpolateFnWatchAction(newValue, oldValue) {
18801873
//special case for class attribute addition + removal
18811874
//so that class changes can tap into the animation
18821875
//hooks provided by the $animate service. Be sure to
18831876
//skip animations when the first digest occurs (when
18841877
//both the new and the old values are the same) since
18851878
//the CSS classes are the non-interpolated values
1886-
if(name === 'class' && interpolationResult != lastInterpolationResult) {
1887-
attr.$updateClass(interpolationResult, lastInterpolationResult);
1879+
if(name === 'class' && newValue != oldValue) {
1880+
attr.$updateClass(newValue, oldValue);
18881881
} else {
1889-
attr.$set(name, interpolationResult);
1882+
attr.$set(name, newValue);
18901883
}
18911884
});
18921885
}

src/ng/directive/select.js

+6-8
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,6 @@ var optionDirective = ['$interpolate', function($interpolate) {
609609
parent = element.parent(),
610610
selectCtrl = parent.data(selectCtrlName) ||
611611
parent.parent().data(selectCtrlName); // in case we are in optgroup
612-
var newString;
613-
var oldString;
614612

615613
if (selectCtrl && selectCtrl.databound) {
616614
// For some reason Opera defaults to true and if not overridden this messes up the repeater.
@@ -621,12 +619,12 @@ var optionDirective = ['$interpolate', function($interpolate) {
621619
}
622620

623621
if (interpolateFn) {
624-
scope.$watchGroup(interpolateFn.expressions, function interpolateWatchAction(newVals, oldVals) {
625-
oldString = newString;
626-
newString = interpolateFn.compute(newVals);
627-
attr.$set('value', newString);
628-
if (oldString) selectCtrl.removeOption(oldString);
629-
selectCtrl.addOption(newString);
622+
scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
623+
attr.$set('value', newVal);
624+
if (oldVal !== newVal) {
625+
selectCtrl.removeOption(oldVal);
626+
}
627+
selectCtrl.addOption(newVal);
630628
});
631629
} else {
632630
selectCtrl.addOption(attr.value);

src/ng/interpolate.js

+67-38
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,24 @@ function $InterpolateProvider() {
114114
* result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult,
115115
* trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that
116116
* provides Strict Contextual Escaping for details.
117-
* @returns {Object} An object describing the interpolation template string.
117+
* @returns {function(context)} an interpolation function which is used to compute the
118+
* interpolated string. The function has these parameters:
118119
*
119-
* The properties of the returned object include:
120-
*
121-
* - `template` — `{string}` — original interpolation template string.
122-
* - `separators` — `{Array.<string>}` — array of separators extracted from the template.
123-
* - `expressions` — `{Array.<string>}` — array of expressions extracted from the template.
124-
* - `compute` — {function(Array)()} — function that when called with an array of values will
125-
* compute the result of interpolation for the given interpolation template and values.
120+
* - `context`: evaluation context for all expressions embedded in the interpolated text
126121
*/
127122
function $interpolate(text, mustHaveExpression, trustedContext) {
128123
var startIndex,
129124
endIndex,
130125
index = 0,
131126
separators = [],
132127
expressions = [],
128+
parseFns = [],
133129
textLength = text.length,
134130
hasInterpolation = false,
135131
hasText = false,
136-
fn,
137132
exp,
138-
concat = [];
133+
concat = [],
134+
lastValuesCache = { values: {}, results: {}};
139135

140136
while(index < textLength) {
141137
if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) &&
@@ -144,6 +140,7 @@ function $InterpolateProvider() {
144140
separators.push(text.substring(index, startIndex));
145141
exp = text.substring(startIndex + startSymbolLength, endIndex);
146142
expressions.push(exp);
143+
parseFns.push($parse(exp));
147144
index = endIndex + endSymbolLength;
148145
hasInterpolation = true;
149146
} else {
@@ -176,31 +173,16 @@ function $InterpolateProvider() {
176173
if (!mustHaveExpression || hasInterpolation) {
177174
concat.length = separators.length + expressions.length;
178175

179-
return extend(function interpolationFn(scope) {
180-
var values = [];
181-
forEach(interpolationFn.expressions, function(expression) {
182-
values.push(scope.$eval(expression));
183-
});
184-
return interpolationFn.compute(values);
185-
}, {
186-
exp: text, //deprecated
187-
template: text,
188-
separators: separators,
189-
expressions: expressions,
190-
compute: function(values) {
191-
for(var i = 0, ii = expressions.length; i < ii; i++) {
192-
concat[2*i] = separators[i];
193-
concat[(2*i)+1] = stringify(values[i]);
194-
}
195-
concat[2*ii] = separators[ii];
196-
return concat.join('');
176+
var compute = function(values) {
177+
for(var i = 0, ii = expressions.length; i < ii; i++) {
178+
concat[2*i] = separators[i];
179+
concat[(2*i)+1] = values[i];
197180
}
198-
});
199-
}
200-
201-
function stringify(value) {
202-
try {
181+
concat[2*ii] = separators[ii];
182+
return concat.join('');
183+
};
203184

185+
var stringify = function (value) {
204186
if (trustedContext) {
205187
value = $sce.getTrusted(trustedContext, value);
206188
} else {
@@ -214,12 +196,59 @@ function $InterpolateProvider() {
214196
}
215197

216198
return value;
199+
};
217200

218-
} catch(err) {
219-
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
220-
err.toString());
221-
$exceptionHandler(newErr);
222-
}
201+
return extend(function interpolationFn(context) {
202+
var scopeId = context.$id || 'notAScope';
203+
var lastValues = lastValuesCache.values[scopeId];
204+
var lastResult = lastValuesCache.results[scopeId];
205+
var i = 0;
206+
var ii = expressions.length;
207+
var values = new Array(ii);
208+
var val;
209+
var inputsChanged = lastResult === undefined ? true: false;
210+
211+
212+
// if we haven't seen this context before, initialize the cache and try to setup
213+
// a cleanup routine that purges the cache when the scope goes away.
214+
if (!lastValues) {
215+
lastValues = [];
216+
inputsChanged = true;
217+
if (context.$on) {
218+
context.$on('$destroy', function() {
219+
lastValuesCache.values[scopeId] = null;
220+
lastValuesCache.results[scopeId] = null;
221+
});
222+
}
223+
}
224+
225+
226+
try {
227+
for (; i < ii; i++) {
228+
val = stringify(parseFns[i](context));
229+
if (val !== lastValues[i]) {
230+
inputsChanged = true;
231+
}
232+
values[i] = val;
233+
}
234+
235+
if (inputsChanged) {
236+
lastValuesCache.values[scopeId] = values;
237+
lastValuesCache.results[scopeId] = lastResult = compute(values);
238+
}
239+
} catch(err) {
240+
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
241+
err.toString());
242+
$exceptionHandler(newErr);
243+
}
244+
245+
return lastResult;
246+
}, {
247+
// all of these properties are undocumented for now
248+
exp: text, //just for compatibility with regular watchers created via $watch
249+
separators: separators,
250+
expressions: expressions
251+
});
223252
}
224253
}
225254

0 commit comments

Comments
 (0)