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

Commit c9ff35c

Browse files
committed
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 344dffb commit c9ff35c

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
@@ -1625,6 +1625,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16251625
};
16261626
}
16271627

1628+
/**
1629+
* A function generator that is used to support both eager and lazy compilation
1630+
* linking function.
1631+
* @param eager
1632+
* @param $compileNodes
1633+
* @param transcludeFn
1634+
* @param maxPriority
1635+
* @param ignoreDirective
1636+
* @param previousCompileContext
1637+
* @returns {Function}
1638+
*/
1639+
function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
1640+
if (eager) {
1641+
return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
1642+
}
1643+
1644+
var compiled;
1645+
1646+
return function() {
1647+
if (!compiled) {
1648+
compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
1649+
1650+
// Null out all of these references in order to make them eligible for garbage collection
1651+
// since this is a potentially long lived closure
1652+
$compileNodes = transcludeFn = previousCompileContext = null;
1653+
}
1654+
1655+
return compiled.apply(this, arguments);
1656+
};
1657+
}
1658+
16281659
/**
16291660
* Once the directives have been collected, their compile functions are executed. This method
16301661
* is responsible for inlining directive templates as well as terminating the application
@@ -1669,6 +1700,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
16691700
replaceDirective = originalReplaceDirective,
16701701
childTranscludeFn = transcludeFn,
16711702
linkFn,
1703+
didScanForMultipleTransclusion = false,
1704+
mightHaveMultipleTransclusionError = false,
16721705
directiveValue;
16731706

16741707
// executes all directives on the current element
@@ -1711,6 +1744,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17111744

17121745
directiveName = directive.name;
17131746

1747+
// If we encounter a condition that can result in transclusion on the directive,
1748+
// then scan ahead in the remaining directives for others that may cause a multiple
1749+
// transclusion error to be thrown during the compilation process. If a matching directive
1750+
// is found, then we know that when we encounter a transcluded directive, we need to eagerly
1751+
// compile the `transclude` function rather than doing it lazily in order to throw
1752+
// exceptions at the correct time
1753+
if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template))
1754+
|| (directive.transclude && !directive.$$tlb))) {
1755+
var candidateDirective;
1756+
1757+
for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
1758+
if ((candidateDirective.transclude && !candidateDirective.$$tlb)
1759+
|| (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template ))) {
1760+
mightHaveMultipleTransclusionError = true;
1761+
break;
1762+
}
1763+
}
1764+
1765+
didScanForMultipleTransclusion = true;
1766+
}
1767+
17141768
if (!directive.templateUrl && directive.controller) {
17151769
directiveValue = directive.controller;
17161770
controllerDirectives = controllerDirectives || createMap();
@@ -1740,7 +1794,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17401794
compileNode = $compileNode[0];
17411795
replaceWith(jqCollection, sliceArgs($template), compileNode);
17421796

1743-
childTranscludeFn = compile($template, transcludeFn, terminalPriority,
1797+
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
17441798
replaceDirective && replaceDirective.name, {
17451799
// Don't pass in:
17461800
// - controllerDirectives - otherwise we'll create duplicates controllers
@@ -1754,7 +1808,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
17541808
} else {
17551809
$template = jqLite(jqLiteClone(compileNode)).contents();
17561810
$compileNode.empty(); // clear contents
1757-
childTranscludeFn = compile($template, transcludeFn);
1811+
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn);
17581812
}
17591813
}
17601814

test/ng/compileSpec.js

+207
Original file line numberDiff line numberDiff line change
@@ -6596,6 +6596,27 @@ describe('$compile', function() {
65966596
});
65976597
});
65986598

6599+
it('should only allow one element transclusion per element when replace directive is in the mix', function() {
6600+
module(function() {
6601+
directive('template', valueFn({
6602+
template: '<p second></p>',
6603+
replace: true
6604+
}));
6605+
directive('first', valueFn({
6606+
transclude: 'element',
6607+
priority: 100
6608+
}));
6609+
directive('second', valueFn({
6610+
transclude: 'element'
6611+
}));
6612+
});
6613+
inject(function($compile) {
6614+
expect(function() {
6615+
$compile('<div template first></div>');
6616+
}).toThrowMinErr('$compile', 'multidir', /Multiple directives \[first, second\] asking for transclusion on: <p .+/);
6617+
});
6618+
});
6619+
65996620

66006621
it('should support transcluded element on root content', function() {
66016622
var comment;
@@ -6892,6 +6913,192 @@ describe('$compile', function() {
68926913
});
68936914

68946915
});
6916+
6917+
it('should lazily compile the contents of directives that are transcluded', function() {
6918+
var innerCompilationCount = 0, transclude;
6919+
6920+
module(function() {
6921+
directive('trans', valueFn({
6922+
transclude: true,
6923+
controller: function($transclude) {
6924+
transclude = $transclude;
6925+
}
6926+
}));
6927+
6928+
directive('inner', valueFn({
6929+
template: '<span>FooBar</span>',
6930+
compile: function() {
6931+
innerCompilationCount +=1;
6932+
}
6933+
}));
6934+
});
6935+
6936+
inject(function($compile, $rootScope) {
6937+
element = $compile('<trans><inner></inner></trans>')($rootScope);
6938+
expect(innerCompilationCount).toBe(0);
6939+
transclude(function(child) { element.append(child); });
6940+
expect(innerCompilationCount).toBe(1);
6941+
expect(element.text()).toBe('FooBar');
6942+
});
6943+
});
6944+
6945+
it('should lazily compile the contents of directives that are transcluded with a template', function() {
6946+
var innerCompilationCount = 0, transclude;
6947+
6948+
module(function() {
6949+
directive('trans', valueFn({
6950+
transclude: true,
6951+
template: '<div>Baz</div>',
6952+
controller: function($transclude) {
6953+
transclude = $transclude;
6954+
}
6955+
}));
6956+
6957+
directive('inner', valueFn({
6958+
template: '<span>FooBar</span>',
6959+
compile: function() {
6960+
innerCompilationCount +=1;
6961+
}
6962+
}));
6963+
});
6964+
6965+
inject(function($compile, $rootScope) {
6966+
element = $compile('<trans><inner></inner></trans>')($rootScope);
6967+
expect(innerCompilationCount).toBe(0);
6968+
transclude(function(child) { element.append(child); });
6969+
expect(innerCompilationCount).toBe(1);
6970+
expect(element.text()).toBe('BazFooBar');
6971+
});
6972+
});
6973+
6974+
it('should lazily compile the contents of directives that are transcluded with a templateUrl', function() {
6975+
var innerCompilationCount = 0, transclude;
6976+
6977+
module(function() {
6978+
directive('trans', valueFn({
6979+
transclude: true,
6980+
templateUrl: 'baz.html',
6981+
controller: function($transclude) {
6982+
transclude = $transclude;
6983+
}
6984+
}));
6985+
6986+
directive('inner', valueFn({
6987+
template: '<span>FooBar</span>',
6988+
compile: function() {
6989+
innerCompilationCount +=1;
6990+
}
6991+
}));
6992+
});
6993+
6994+
inject(function($compile, $rootScope, $httpBackend) {
6995+
$httpBackend.expectGET('baz.html').respond('<div>Baz</div>');
6996+
element = $compile('<trans><inner></inner></trans>')($rootScope);
6997+
$httpBackend.flush();
6998+
6999+
expect(innerCompilationCount).toBe(0);
7000+
transclude(function(child) { element.append(child); });
7001+
expect(innerCompilationCount).toBe(1);
7002+
expect(element.text()).toBe('BazFooBar');
7003+
});
7004+
});
7005+
7006+
it('should lazily compile the contents of directives that are transclude element', function() {
7007+
var innerCompilationCount = 0, transclude;
7008+
7009+
module(function() {
7010+
directive('trans', valueFn({
7011+
transclude: 'element',
7012+
controller: function($transclude) {
7013+
transclude = $transclude;
7014+
}
7015+
}));
7016+
7017+
directive('inner', valueFn({
7018+
template: '<span>FooBar</span>',
7019+
compile: function() {
7020+
innerCompilationCount +=1;
7021+
}
7022+
}));
7023+
});
7024+
7025+
inject(function($compile, $rootScope) {
7026+
element = $compile('<div><trans><inner></inner></trans></div>')($rootScope);
7027+
expect(innerCompilationCount).toBe(0);
7028+
transclude(function(child) { element.append(child); });
7029+
expect(innerCompilationCount).toBe(1);
7030+
expect(element.text()).toBe('FooBar');
7031+
});
7032+
});
7033+
7034+
it('should lazily compile transcluded directives with ngIf on them', function() {
7035+
var innerCompilationCount = 0, outerCompilationCount = 0, transclude;
7036+
7037+
module(function() {
7038+
directive('outer', valueFn({
7039+
transclude: true,
7040+
compile: function() {
7041+
outerCompilationCount += 1;
7042+
},
7043+
controller: function($transclude) {
7044+
transclude = $transclude;
7045+
}
7046+
}));
7047+
7048+
directive('inner', valueFn({
7049+
template: '<span>FooBar</span>',
7050+
compile: function() {
7051+
innerCompilationCount +=1;
7052+
}
7053+
}));
7054+
});
7055+
7056+
inject(function($compile, $rootScope) {
7057+
$rootScope.shouldCompile = false;
7058+
7059+
element = $compile('<div><outer ng-if="shouldCompile"><inner></inner></outer></div>')($rootScope);
7060+
expect(outerCompilationCount).toBe(0);
7061+
expect(innerCompilationCount).toBe(0);
7062+
expect(transclude).toBeUndefined();
7063+
$rootScope.$apply('shouldCompile=true');
7064+
expect(outerCompilationCount).toBe(1);
7065+
expect(innerCompilationCount).toBe(0);
7066+
expect(transclude).toBeDefined();
7067+
transclude(function(child) { element.append(child); });
7068+
expect(outerCompilationCount).toBe(1);
7069+
expect(innerCompilationCount).toBe(1);
7070+
expect(element.text()).toBe('FooBar');
7071+
});
7072+
});
7073+
7074+
it('should eagerly compile multiple directives with transclusion and templateUrl/replace', function() {
7075+
var innerCompilationCount = 0;
7076+
7077+
module(function() {
7078+
directive('outer', valueFn({
7079+
transclude: true
7080+
}));
7081+
7082+
directive('outer', valueFn({
7083+
templateUrl: 'inner.html',
7084+
replace: true
7085+
}));
7086+
7087+
directive('inner', valueFn({
7088+
compile: function() {
7089+
innerCompilationCount +=1;
7090+
}
7091+
}));
7092+
});
7093+
7094+
inject(function($compile, $rootScope, $httpBackend) {
7095+
$httpBackend.expectGET('inner.html').respond('<inner></inner>');
7096+
element = $compile('<outer></outer>')($rootScope);
7097+
$httpBackend.flush();
7098+
7099+
expect(innerCompilationCount).toBe(1);
7100+
});
7101+
});
68957102
});
68967103

68977104

0 commit comments

Comments
 (0)