angularjs source code Notes (1.1) -- direct compile

Posted by jcampbell1 on Thu, 06 Jan 2022 01:10:17 +0100

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. <1> return publicLinkFn, the fn returned by <2> in the fn.
  2. <2> return compositeLinkFn, the fn returned by <3> in the fn.
  3. <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,

  1. Packaging node
  2. Call compileNodes
  3. 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;
  1. clone for jq
  2. 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