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

Commit 652b83e

Browse files
dchermanlgalfaso
authored andcommitted
perf($compile): Lazily compile the transclude function
For transcluded directives, the transclude function can be lazily compiled most of the time since the contents will not be needed until the `transclude` function was actually invoked. For example, the `transclude` function that is passed to `ng-if` or `ng-switch-when` does not need to be invoked until the condition that it's bound to has been matched. For complex trees or switch statements, this can represent significant performance gains since compilation of branches is deferred, and that compilation may never actually happen if it isn't needed. There are two instances where compilation will not be lazy; when we scan ahead in the array of directives to be processed and find at least two of the following: * A directive that is transcluded and does not allow multiple transclusion * A directive that has templateUrl and replace: true * A directive that has a template and replace: true In both of those cases, we will need to continue eager compilation in order to generate the multiple transclusion exception at the correct time.
1 parent 20cf7d5 commit 652b83e

File tree

2 files changed

+263
-2
lines changed

2 files changed

+263
-2
lines changed

src/ng/compile.js

+56-2
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16461646
};
16471647
}
16481648

1649+
/**
1650+
* A function generator that is used to support both eager and lazy compilation
1651+
* linking function.
1652+
* @param eager
1653+
* @param $compileNodes
1654+
* @param transcludeFn
1655+
* @param maxPriority
1656+
* @param ignoreDirective
1657+
* @param previousCompileContext
1658+
* @returns {Function}
1659+
*/
1660+
function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
1661+
if (eager) {
1662+
return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
1663+
}
1664+
1665+
var compiled;
1666+
1667+
return function() {
1668+
if (!compiled) {
1669+
compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
1670+
1671+
// Null out all of these references in order to make them eligible for garbage collection
1672+
// since this is a potentially long lived closure
1673+
$compileNodes = transcludeFn = previousCompileContext = null;
1674+
}
1675+
1676+
return compiled.apply(this, arguments);
1677+
};
1678+
}
1679+
16491680
/**
16501681
* Once the directives have been collected, their compile functions are executed. This method
16511682
* is responsible for inlining directive templates as well as terminating the application
@@ -1690,6 +1721,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16901721
replaceDirective = originalReplaceDirective,
16911722
childTranscludeFn = transcludeFn,
16921723
linkFn,
1724+
didScanForMultipleTransclusion = false,
1725+
mightHaveMultipleTransclusionError = false,
16931726
directiveValue;
16941727

16951728
// executes all directives on the current element
@@ -1732,6 +1765,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17321765

17331766
directiveName = directive.name;
17341767

1768+
// If we encounter a condition that can result in transclusion on the directive,
1769+
// then scan ahead in the remaining directives for others that may cause a multiple
1770+
// transclusion error to be thrown during the compilation process. If a matching directive
1771+
// is found, then we know that when we encounter a transcluded directive, we need to eagerly
1772+
// compile the `transclude` function rather than doing it lazily in order to throw
1773+
// exceptions at the correct time
1774+
if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template))
1775+
|| (directive.transclude && !directive.$$tlb))) {
1776+
var candidateDirective;
1777+
1778+
for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
1779+
if ((candidateDirective.transclude && !candidateDirective.$$tlb)
1780+
|| (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) {
1781+
mightHaveMultipleTransclusionError = true;
1782+
break;
1783+
}
1784+
}
1785+
1786+
didScanForMultipleTransclusion = true;
1787+
}
1788+
17351789
if (!directive.templateUrl && directive.controller) {
17361790
directiveValue = directive.controller;
17371791
controllerDirectives = controllerDirectives || createMap();
@@ -1761,7 +1815,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17611815
compileNode = $compileNode[0];
17621816
replaceWith(jqCollection, sliceArgs($template), compileNode);
17631817

1764-
childTranscludeFn = compile($template, transcludeFn, terminalPriority,
1818+
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
17651819
replaceDirective && replaceDirective.name, {
17661820
// Don't pass in:
17671821
// - controllerDirectives - otherwise we'll create duplicates controllers
@@ -1775,7 +1829,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17751829
} else {
17761830
$template = jqLite(jqLiteClone(compileNode)).contents();
17771831
$compileNode.empty(); // clear contents
1778-
childTranscludeFn = compile($template, transcludeFn);
1832+
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn);
17791833
}
17801834
}
17811835

test/ng/compileSpec.js

+207
Original file line numberDiff line numberDiff line change
@@ -6756,6 +6756,27 @@ describe('$compile', function() {
67566756
});
67576757
});
67586758

6759+
it('should only allow one element transclusion per element when replace directive is in the mix', function() {
6760+
module(function() {
6761+
directive('template', valueFn({
6762+
template: '<p second></p>',
6763+
replace: true
6764+
}));
6765+
directive('first', valueFn({
6766+
transclude: 'element',
6767+
priority: 100
6768+
}));
6769+
directive('second', valueFn({
6770+
transclude: 'element'
6771+
}));
6772+
});
6773+
inject(function($compile) {
6774+
expect(function() {
6775+
$compile('<div template first></div>');
6776+
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second\] asking for transclusion on: <p .+/);
6777+
});
6778+
});
6779+
67596780

67606781
it('should support transcluded element on root content', function() {
67616782
var comment;
@@ -7052,6 +7073,192 @@ describe('$compile', function() {
70527073
});
70537074

70547075
});
7076+
7077+
it('should lazily compile the contents of directives that are transcluded', function() {
7078+
var innerCompilationCount = 0, transclude;
7079+
7080+
module(function() {
7081+
directive('trans', valueFn({
7082+
transclude: true,
7083+
controller: function($transclude) {
7084+
transclude = $transclude;
7085+
}
7086+
}));
7087+
7088+
directive('inner', valueFn({
7089+
template: '<span>FooBar</span>',
7090+
compile: function() {
7091+
innerCompilationCount +=1;
7092+
}
7093+
}));
7094+
});
7095+
7096+
inject(function($compile, $rootScope) {
7097+
element = $compile('<trans><inner></inner></trans>')($rootScope);
7098+
expect(innerCompilationCount).toBe(0);
7099+
transclude(function(child) { element.append(child); });
7100+
expect(innerCompilationCount).toBe(1);
7101+
expect(element.text()).toBe('FooBar');
7102+
});
7103+
});
7104+
7105+
it('should lazily compile the contents of directives that are transcluded with a template', function() {
7106+
var innerCompilationCount = 0, transclude;
7107+
7108+
module(function() {
7109+
directive('trans', valueFn({
7110+
transclude: true,
7111+
template: '<div>Baz</div>',
7112+
controller: function($transclude) {
7113+
transclude = $transclude;
7114+
}
7115+
}));
7116+
7117+
directive('inner', valueFn({
7118+
template: '<span>FooBar</span>',
7119+
compile: function() {
7120+
innerCompilationCount +=1;
7121+
}
7122+
}));
7123+
});
7124+
7125+
inject(function($compile, $rootScope) {
7126+
element = $compile('<trans><inner></inner></trans>')($rootScope);
7127+
expect(innerCompilationCount).toBe(0);
7128+
transclude(function(child) { element.append(child); });
7129+
expect(innerCompilationCount).toBe(1);
7130+
expect(element.text()).toBe('BazFooBar');
7131+
});
7132+
});
7133+
7134+
it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() {
7135+
var innerCompilationCount = 0, transclude;
7136+
7137+
module(function() {
7138+
directive('trans', valueFn({
7139+
transclude: true,
7140+
templateUrl: 'baz.html',
7141+
controller: function($transclude) {
7142+
transclude = $transclude;
7143+
}
7144+
}));
7145+
7146+
directive('inner', valueFn({
7147+
template: '<span>FooBar</span>',
7148+
compile: function() {
7149+
innerCompilationCount +=1;
7150+
}
7151+
}));
7152+
});
7153+
7154+
inject(function($compile, $rootScope, $httpBackend) {
7155+
$httpBackend.expectGET('baz.html').respond('<div>Baz</div>');
7156+
element = $compile('<trans><inner></inner></trans>')($rootScope);
7157+
$httpBackend.flush();
7158+
7159+
expect(innerCompilationCount).toBe(0);
7160+
transclude(function(child) { element.append(child); });
7161+
expect(innerCompilationCount).toBe(1);
7162+
expect(element.text()).toBe('BazFooBar');
7163+
});
7164+
});
7165+
7166+
it('should lazily compile the contents of directives that are transclude element', function() {
7167+
var innerCompilationCount = 0, transclude;
7168+
7169+
module(function() {
7170+
directive('trans', valueFn({
7171+
transclude: 'element',
7172+
controller: function($transclude) {
7173+
transclude = $transclude;
7174+
}
7175+
}));
7176+
7177+
directive('inner', valueFn({
7178+
template: '<span>FooBar</span>',
7179+
compile: function() {
7180+
innerCompilationCount +=1;
7181+
}
7182+
}));
7183+
});
7184+
7185+
inject(function($compile, $rootScope) {
7186+
element = $compile('<div><trans><inner></inner></trans></div>')($rootScope);
7187+
expect(innerCompilationCount).toBe(0);
7188+
transclude(function(child) { element.append(child); });
7189+
expect(innerCompilationCount).toBe(1);
7190+
expect(element.text()).toBe('FooBar');
7191+
});
7192+
});
7193+
7194+
it('should lazily compile transcluded directives with ngIf on them', function() {
7195+
var innerCompilationCount = 0, outerCompilationCount = 0, transclude;
7196+
7197+
module(function() {
7198+
directive('outer', valueFn({
7199+
transclude: true,
7200+
compile: function() {
7201+
outerCompilationCount += 1;
7202+
},
7203+
controller: function($transclude) {
7204+
transclude = $transclude;
7205+
}
7206+
}));
7207+
7208+
directive('inner', valueFn({
7209+
template: '<span>FooBar</span>',
7210+
compile: function() {
7211+
innerCompilationCount +=1;
7212+
}
7213+
}));
7214+
});
7215+
7216+
inject(function($compile, $rootScope) {
7217+
$rootScope.shouldCompile = false;
7218+
7219+
element = $compile('<div><outer ng-if="shouldCompile"><inner></inner></outer></div>')($rootScope);
7220+
expect(outerCompilationCount).toBe(0);
7221+
expect(innerCompilationCount).toBe(0);
7222+
expect(transclude).toBeUndefined();
7223+
$rootScope.$apply('shouldCompile=true');
7224+
expect(outerCompilationCount).toBe(1);
7225+
expect(innerCompilationCount).toBe(0);
7226+
expect(transclude).toBeDefined();
7227+
transclude(function(child) { element.append(child); });
7228+
expect(outerCompilationCount).toBe(1);
7229+
expect(innerCompilationCount).toBe(1);
7230+
expect(element.text()).toBe('FooBar');
7231+
});
7232+
});
7233+
7234+
it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() {
7235+
var innerCompilationCount = 0;
7236+
7237+
module(function() {
7238+
directive('outer', valueFn({
7239+
transclude: true
7240+
}));
7241+
7242+
directive('outer', valueFn({
7243+
templateUrl: 'inner.html',
7244+
replace: true
7245+
}));
7246+
7247+
directive('inner', valueFn({
7248+
compile: function() {
7249+
innerCompilationCount +=1;
7250+
}
7251+
}));
7252+
});
7253+
7254+
inject(function($compile, $rootScope, $httpBackend) {
7255+
$httpBackend.expectGET('inner.html').respond('<inner></inner>');
7256+
element = $compile('<outer></outer>')($rootScope);
7257+
$httpBackend.flush();
7258+
7259+
expect(innerCompilationCount).toBe(1);
7260+
});
7261+
});
70557262
});
70567263

70577264

0 commit comments

Comments
 (0)