Skip to content

Commit 547191a

Browse files
rodyhaddadIgorMinar
authored andcommitted
refactor($interpolate): optimize watched $interpolate functions perf
1 parent 3fc8017 commit 547191a

File tree

3 files changed

+130
-27
lines changed

3 files changed

+130
-27
lines changed

src/ng/interpolate.js

+79-25
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,11 @@ function $InterpolateProvider() {
127127
var startIndex,
128128
endIndex,
129129
index = 0,
130-
parts = [],
131130
length = text.length,
132131
hasInterpolation = false,
133-
fn,
132+
fn = null,
134133
exp,
135-
concat = [];
134+
parts = [];
136135

137136
while(index < length) {
138137
if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) &&
@@ -149,10 +148,9 @@ function $InterpolateProvider() {
149148
}
150149
}
151150

152-
if (!(length = parts.length)) {
151+
if (!parts.length) {
153152
// we added, nothing, must have been an empty string.
154153
parts.push('');
155-
length = 1;
156154
}
157155

158156
// Concatenating expressions makes it hard to reason about whether some combination of
@@ -169,25 +167,29 @@ function $InterpolateProvider() {
169167
}
170168

171169
if (!mustHaveExpression || hasInterpolation) {
172-
concat.length = length;
173-
fn = function(context) {
170+
var concat = new Array(parts.length),
171+
expressions = {};
172+
forEach(parts, function (value, index) {
173+
if (isFunction(value)) {
174+
expressions[index] = value;
175+
concat[index] = '';
176+
} else {
177+
concat[index] = value;
178+
}
179+
});
180+
// computes all the interpolations and returns the resulting string
181+
// a specific index might already be computed (cz of the scope's dirty-checking),
182+
// and so its expression shouldn't be executed a 2nd time
183+
// also populates the lastValues of custom watchers for internal dirty-checking
184+
var getTextValue = function (scope, computedIndex, computedValue, lastValues) {
174185
try {
175-
for(var i = 0, ii = length, part; i<ii; i++) {
176-
if (typeof (part = parts[i]) == 'function') {
177-
part = part(context);
178-
if (trustedContext) {
179-
part = $sce.getTrusted(trustedContext, part);
180-
} else {
181-
part = $sce.valueOf(part);
182-
}
183-
if (part === null || isUndefined(part)) {
184-
part = '';
185-
} else if (typeof part != 'string') {
186-
part = toJson(part);
187-
}
188-
}
189-
concat[i] = part;
190-
}
186+
forEach(expressions, function (expression, index) {
187+
concat[index] = index == computedIndex
188+
? computedValue
189+
: getStringValue(expression(scope));
190+
191+
if (lastValues) lastValues[index] = concat[index];
192+
});
191193
return concat.join('');
192194
}
193195
catch(err) {
@@ -196,10 +198,63 @@ function $InterpolateProvider() {
196198
$exceptionHandler(newErr);
197199
}
198200
};
201+
var getStringValue = function (value) {
202+
value = trustedContext
203+
? $sce.getTrusted(trustedContext, value)
204+
: $sce.valueOf(value);
205+
206+
if (value === null || isUndefined(value)) {
207+
return '';
208+
}
209+
return isString(value) ? value : toJson(value);
210+
};
211+
212+
fn = function(scope) {
213+
return getTextValue(scope);
214+
};
199215
fn.exp = text;
200216
fn.parts = parts;
201-
return fn;
217+
218+
// watches each interpolation separately for performance
219+
fn.$$beWatched = function (scope, origListener, objectEquality) {
220+
var lastTextValue, lastValues = {}, watchersRm = [];
221+
222+
forEach(expressions, function (expression, index) {
223+
watchersRm.push(scope.$watch(function watchInterpolatedExpr(scope) {
224+
try {
225+
return getStringValue(expression(scope));
226+
} catch (err) {
227+
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}",
228+
text, err.toString());
229+
$exceptionHandler(newErr);
230+
}
231+
}, listenerOf(index), objectEquality));
232+
});
233+
234+
function listenerOf(index) {
235+
return function interpolatedExprListener(value, oldValue) {
236+
// we only invoke the origListener if the current value
237+
// is not equal to the last computed value
238+
// ex: if in `{{a}}-{{b}}` both values change in a digest,
239+
// the listener of `a` gets invoked first, we compute the string
240+
// and invoke the origListener once,
241+
// and ignore it when the listener of `b` gets triggered
242+
// (unless the value of `b` changes again since the last computation)
243+
if (value !== lastValues[index]) {
244+
var textValue = getTextValue(scope, index, value, lastValues);
245+
origListener.call(this, textValue,
246+
value === oldValue ? textValue : lastTextValue, scope);
247+
lastTextValue = textValue;
248+
}
249+
};
250+
}
251+
252+
return function compositeWatchersRm() {
253+
forEach(watchersRm, function(wRm){ wRm(); });
254+
};
255+
};
202256
}
257+
return fn;
203258
}
204259

205260

@@ -239,4 +294,3 @@ function $InterpolateProvider() {
239294
return $interpolate;
240295
}];
241296
}
242-

src/ng/rootScope.js

+3
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,9 @@ function $RootScopeProvider(){
315315
* @returns {function()} Returns a deregistration function for this listener.
316316
*/
317317
$watch: function(watchExp, listener, objectEquality) {
318+
if (isFunction(watchExp.$$beWatched)) {
319+
return watchExp.$$beWatched(this, listener, objectEquality, watchExp);
320+
}
318321
var scope = this,
319322
get = compileToFn(watchExp, 'watch'),
320323
array = scope.$$watchers,

test/ng/interpolateSpec.js

+48-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('$interpolate', function() {
1010

1111
it('should return undefined when there are no bindings and textOnly is set to true',
1212
inject(function($interpolate) {
13-
expect($interpolate('some text', true)).toBeUndefined();
13+
expect($interpolate('some text', true)).toBeNull();
1414
}));
1515

1616
it('should suppress falsy objects', inject(function($interpolate) {
@@ -70,7 +70,7 @@ describe('$interpolate', function() {
7070
describe('interpolating in a trusted context', function() {
7171
var sce;
7272
beforeEach(function() {
73-
function log() {};
73+
function log() {}
7474
var fakeLog = {log: log, warn: log, info: log, error: log};
7575
module(function($provide, $sceProvider) {
7676
$provide.value('$log', fakeLog);
@@ -274,4 +274,50 @@ describe('$interpolate', function() {
274274
}));
275275
});
276276

277+
describe('custom $$beWatched', function () {
278+
it('should call the listener correctly when values change during digest',
279+
inject(function ($rootScope, $interpolate) {
280+
var nbCalls = 0, value;
281+
$rootScope.$watch($interpolate('{{a}}-{{b}}'), function (_value) {
282+
value = _value;
283+
switch(++nbCalls) {
284+
case 1:
285+
case 2:
286+
$rootScope.b++;
287+
break;
288+
case 3:
289+
case 4:
290+
$rootScope.a++;
291+
break;
292+
}
293+
});
294+
$rootScope.$apply(function () {
295+
$rootScope.a = $rootScope.b = 0;
296+
});
297+
expect(value).toBe("2-2");
298+
expect(nbCalls).toBe(5);
299+
}));
300+
301+
it('should call the listener correctly when the interpolation is watched multiple times',
302+
inject(function ($rootScope, $interpolate) {
303+
var interpolateFn = $interpolate('{{a}}-{{b}}'), nbCalls = 0;
304+
$rootScope.$watch(interpolateFn, function(){
305+
nbCalls++;
306+
});
307+
$rootScope.$watch(interpolateFn, function(){
308+
nbCalls++;
309+
});
310+
311+
$rootScope.$apply(function () {
312+
$rootScope.a = $rootScope.b = 0;
313+
});
314+
expect(nbCalls).toBe(2);
315+
316+
$rootScope.$apply(function () {
317+
$rootScope.a = $rootScope.b = 1;
318+
});
319+
expect(nbCalls).toBe(4);
320+
}));
321+
})
322+
277323
});

0 commit comments

Comments
 (0)