Compile (1)
1. Structure
Like other service s, compile needs to register a provider -- the compileprovider is the provider that compile registers into angular. such
The main call paths are as follows:
compile<1> -> compileNodes<2> -> applyDirectivesToNode<3>
- <1> return publicLinkFn, the fn returned by <2> in the fn.
- <2> return compositeLinkFn, the fn returned by <3> in the fn.
- <3> return nodeLinkFn
The main line is the so-called compile phase, and calling the returned fn enters the link phase
2. Compile phase
2.1. compile()
compile is the portal fn, which mainly does three things,
- Packaging node
- Call compileNodes
- Return publicLinkFn for the link phase to call
// Wrap text as < span > text</span> forEach($compileNodes, function(node, index){ if (node.nodeType == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0]; } });
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext);
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)
2.2. compileNodes()
The parameter will be passed into nodeList, and then each node will be executed circularly. The execution is as follows:
1). Collect directives
directives = collectDirectives(nodeList[i]....);
2). Execute applydirectionstonode (subsequent detailed analysis)
nodeLinkFn = applyDirectivesToNode(directives, nodeList[i]....)
3). A recursive call executes compileNodes on childNodes
childLinkFn = compileNodes(childNodes...)
4). Return compositeLinkFn
2.3. applyDirectivesToNode()
The parameters of fn, (1) directives, (2)compileNode, others omitted
1). That is, compile the compilenode in turn for the collected directives array
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
Here, directive is the defined instruction, such as:
module.directive('xxx', function () { return { compile: function () { return function postLinkFn() {}; } }; });
The returned object is direct. As shown in the above example, compile returns a postLink fn. Of course, the complete object should be an object containing preLink and postLink, such as:
{ compile: function () { return { pre: function () {}, post: function () {} }; } }
2). The returned linkFn is collected into preLinkFns and postLinkFns for subsequent calls
addLinkFns(...)
There is an isFunction judgment here, that is, if only function is returned, it will be collected as post. If it is an object, it will be pre or post according to the field
if (isFunction(linkFn)) { addLinkFns(null, linkFn, attrStart, attrEnd); } else if (linkFn) { addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); }
3). Finally, the nodeLinkFn function is returned
3. Link phase
compile.publicLinkFn -> compileNodes.compositeLinkFn -> applyDirectivesToNode.nodeLinkFn
3.1. publicLinkFn()
function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)
1). Each element is bound with a scope
// Attach scope only to non-text nodes. for(var i = 0, ii = $linkNode.length; i<ii; i++) { var node = $linkNode[i], nodeType = node.nodeType; if (nodeType === 1 /* element */ || nodeType === 9 /* document */) { $linkNode.eq(i).data('$scope', scope); } }
2). compositeLinkFn returned before calling
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
3.2. compositeLinkFn()
function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn)
The main task of compositeLinkFn is to execute the nodeLinkFn returned by applydirectionstonode and the compositeLinkFn returned by recursively calling compileNodes(childNodes)
if (nodeLinkFn) { //Judge whether the directive is a defined scope:true and process it if (nodeLinkFn.scope) { childScope = scope.$new(); $node.data('$scope', childScope); } else { childScope = scope; } //For the processing of transcluster, follow-up analysis if ( nodeLinkFn.transcludeOnThisElement ) { childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn); } else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; } else if (!parentBoundTranscludeFn && transcludeFn) { childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); } else { childBoundTranscludeFn = null; } nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); } else if (childLinkFn) { //childLinkFn === compositeLinkFn childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn); }
//Where there are some details, why copy a node array? //Because the link phase will add and delete nodeList, which will affect the execution of linkFn array //The copied array can ensure that each linkFn will be executed accurately var nodeListLength = nodeList.length, stableNodeList = new Array(nodeListLength); for (i = 0; i < nodeListLength; i++) { stableNodeList[i] = nodeList[i]; }
3.3. nodeLinkFn()
nodeLinkFn is the pre and post methods collected after executing many previous direct compile
// Resolve @ = & in the scope definition to generate an isolateScope forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { var match = definition.match(LOCAL_REGEXP) || [], attrName = match[3] || scopeName, optional = (match[2] == '?'), mode = match[1], // @, =, or & lastValue, parentGet, parentSet, compare; isolateScope.$$isolateBindings[scopeName] = mode + attrName; switch (mode) { case '@': break; case '=': break; case '&': break; default: throw $compileMinErr('iscp', "Invalid isolate scope definition for directive '{0}'." + " Definition: {... {1}: '{2}' ...}", newIsolateScopeDirective.name, scopeName, definition); } })
Then execute controllerfns > prelinkfns > recursive childnodelingfn > postlinkfns
This explains that the order of link, compile and Ctrl in direct is a.ctrl > a.prelink > a.ctrl > a.prelink > a.postlink > a.postlink
A is the child node of A
1) controllers execution
if (controllerDirectives) { forEach(controllerDirectives, function(directive) { var locals = { $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, $element: $element, $attrs: attrs, $transclude: transcludeFn }, controllerInstance; controller = directive.controller; // When configuring controller: @, use the name configured in attr if (controller == '@') { controller = attrs[directive.name]; } //Instantiate controller controllerInstance = $controller(controller, locals); elementControllers[directive.name] = controllerInstance; if (!hasElementTranscludeDirective) { $element.data('$' + directive.name + 'Controller', controllerInstance); } // When configuring controllerAs, bind the instance to the scope if (directive.controllerAs) { locals.$scope[directive.controllerAs] = controllerInstance; } }); }
2) preLink execution
// PRELINKING for(i = 0, ii = preLinkFns.length; i < ii; i++) { try { linkFn = preLinkFns[i]; linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); } catch (e) { $exceptionHandler(e, startingTag($element)); } }
getControllers() is the ctrl used to get the driective defined in the directive
3) childLinkFn
childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
4) postLink
// POSTLINKING for(i = postLinkFns.length - 1; i >= 0; i--) { try { linkFn = postLinkFns[i]; linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn); } catch (e) { $exceptionHandler(e, startingTag($element)); } }
All linkFn (pre and post) parameters are the same
function link (scope, element, attrs, ctrls, transclude);
4. transclude
4.1 definition and configuration of transcluster
Recall the transcluster configuration first
{ transclude: true // or 'element' }
- When configuring an element, the entire element is transclude d
- When the configuration is true, only the child elements of the element are transiclude
4.2 main source code of transcloud
It is also a call chain. The final call entry is in the user-defined link, for example:
{ link: function (scope, el, attrs, ctrls, transclude) { transclude(); } }
Where is the parameter passed in?
Intercept the code executing postLink in nodeLinkFn (the same is true for preLink, which is omitted)
linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs, linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn);
Is the last parameter, so what is the last parameter?
// boundTranscludeFn is a parameter of nodeLinkFn // function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) // Indicates that when boundTranscludeFn exists, the controllersBoundTransclude is assigned to transcludeFn transcludeFn = boundTranscludeFn && controllersBoundTransclude; //... (omit intermediate code) // Two things were handled: // 1. When there is no parameter or one parameter, scope=undefined // 2. Assign the controllers on this element to the third parameter function controllersBoundTransclude(scope, cloneAttachFn) { var transcludeControllers; // no scope passed if (arguments.length < 2) { cloneAttachFn = scope; scope = undefined; } if (hasElementTranscludeDirective) { transcludeControllers = elementControllers; } return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers); }
In this way, the parameter transcludeFn passed in link is actually the parameter boundTranscludeFn of nodeLinkFn, but the next parameter processing is done
As you can see from the above, nodeLinkFn is invoked in compositeLinkFn, and the parameter is passed in here. The code is as follows.
// When the element defines directive and is configured with translate // Call createbundtranscludefn to generate childBoundTranscludeFn,! be careful! The parameter passed in is nodelinkfn transclude if (nodeLinkFn.transcludeOnThisElement) { childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn); } // When the parent of this elementd defines the directive of the include // Directly use parent transcludeFn parentBoundTranscludeFn else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; } else if (!parentBoundTranscludeFn && transcludeFn) { childBoundTranscludeFn = createBoundTranscludeFn(scope, transcludeFn); } else { childBoundTranscludeFn = null; } nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn); // ... // transcludeFn is nodelinkfn in the first if case transclude // The previous boundtranscludefn is the parent boundtranscludefn function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) { var scopeCreated = false; // If the scope is passed in, the passed in parameters will be used. If there is no scope, the current scope will be used$ new if (!transcludedScope) { transcludedScope = scope.$new(); transcludedScope.$$transcluded = true; scopeCreated = true; } var clone = transcludeFn(transcludedScope, cloneFn, controllers, previousBoundTranscludeFn); if (scopeCreated) { clone.on('$destroy', function() { transcludedScope.$destroy(); }); } return clone; }; return boundTranscludeFn; }
So from the code, you can see that the next scope is processed, and the $destroy event is monitored for destruction, and then the second parameter transcludeFn passed in is called
And transcludeFn is nodeLinkFn Translate, return to the place where nodeLinkFn is generated -- applydirectionstonode()
// When configuring translate: 'element', the entire element is compile d // Configure translate: when true, it is a child element to compile if (directiveValue == 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; $template = groupScan(compileNode, attrStart, attrEnd); $compileNode = templateAttrs.$$element = jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); compileNode = $compileNode[0]; replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); // Recursively call compile to return publicLinkFn // Pass in the priority of the current directive as the termination priority to prevent an endless loop childTranscludeFn = compile($template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { nonTlbTranscludeDirective: nonTlbTranscludeDirective }); } else { $template = jqLite(jqLiteClone(compileNode)).contents(); $compileNode.empty(); // clear contents childTranscludeFn = compile($template, transcludeFn); } // ... nodeLinkFn.transclude = childTranscludeFn;
Therefore, childTranscludeFn is actually the publicLinkFn returned by compile. Analysis conclusion: transcludeFn is actually calling publicLinkFn
4.3 inheritance of transcludefn
When the template contains a directive, how to get $exclude (i.e. publicLinkFn of the original childNode of the parent) from the link of the child directive to call
The following code exists in nodeLinkFn
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
The boundTranscludeFn has not been packaged by controllersBoundTransclude(). Because the controllers corresponding to the directivity of each element are different, it needs to be used and adjusted now
parentBoundTranscludeFn passed into publicLinkFn
function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)
Then, it is whitewashed into childbound transclude fn in compositeLinkFn, and finally flows into the parameter $translate of link for use
else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) { childBoundTranscludeFn = parentBoundTranscludeFn; } nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
4.4 application
Thus, when the directive of transclude is defined, the link method can call transcludeFn to obtain the child elements after compile and link, such as
directive('myDir', function () { return { transclude: true, replace: true, template: '<div class="my-dir"></div>' link: function (scope, element, attrs, ctrls, transcludeFn) { var childNodes = transcludeFn(scope); childNodes.addClass('my-child-nodes'); element.append(childNodes); } } }); /** before <my-dir> <div>1</div> <div>2</div> <div>3</div> </my-dir> **/ /** after <div class="my-dir"> <div class="my-child-nodes">1</div> <div class="my-child-nodes">2</div> <div class="my-child-nodes">3</div> </div> **/
You can think of ng transcclude
var ngTranscludeDirective = ngDirective({ link: function($scope, $element, $attrs, controller, $transclude) { if (!$transclude) { throw minErr('ngTransclude')('orphan', 'Illegal use of ngTransclude directive in the template! ' + 'No parent directive that requires a transclusion found. ' + 'Element: {0}', startingTag($element)); } $transclude(function(clone) { $element.empty(); $element.append(clone); }); } });
cloneFn is used here. See the following for cloneFn:
var $linkNode = cloneConnectFn ? JQLitePrototype.clone.call($compileNodes) : $compileNodes; // ... if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); return $linkNode;
- clone for jq
- Call cloneFn
Here I have a question: why clone first? Hope to know the guidance, thank you!
link
angularjs source code Notes (1.1) -- direct compile
angularjs source code Notes (1.2) -- direct template
angularjs source code Notes (2)--inject
angularjs source code Notes (3)--scope