angularjs源码笔记(1.1)--directive compile

2022-01-04 16:57:07 浏览数 (1)

Compile (1)

1. 结构

compile跟其他service一样都需注册一个provider--CompileProvider就是compile注册进angular的provider。这样

主要的调用路径如下:

代码语言:javascript复制
compile<1> -> compileNodes<2> -> applyDirectivesToNode<3>
  1. <1> return publicLinkFn, 该fn中调用 <2>返回的fn
  2. <2> return compositeLinkFn, 该fn中调用<3>返回的fn
  3. <3> return nodeLinkFn

主线就是所说的compile阶段,而对返回的fn进行调用进入link阶段

2. Compile阶段

2.1. compile()

compile为入口fn,主要做3个事情,

  1. 包装node
  2. 调用compileNodes
  3. 返回publicLinkFn供link阶段调用
代码语言:javascript复制
// 将text包装成<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];
  }
});
代码语言:javascript复制
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes,
                           maxPriority, ignoreDirective, previousCompileContext);
代码语言:javascript复制
return function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)

2.2. compileNodes()

参数会传入nodeList, 然后循环执行每个node,执行的事情如下:

1). 收集directives

代码语言:javascript复制
directives = collectDirectives(nodeList[i]....);

2). 执行applyDirectivesToNode(后续详细分析)

代码语言:javascript复制
nodeLinkFn = applyDirectivesToNode(directives, nodeList[i]....)

3). 递归调用执行childNodes上的compileNodes

代码语言:javascript复制
childLinkFn = compileNodes(childNodes...)

4). 返回 compositeLinkFn

2.3. applyDirectivesToNode()

该fn的参数,(1) directives, (2)compileNode, 其他略

1). 即对collectDirectives收集过来directives数组依次编译(compile)compileNode

代码语言:javascript复制
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);

这里directive为定义的指令,如:

代码语言:javascript复制
module.directive('xxx', function () {
  return {
    compile: function () {
      return function postLinkFn() {};
    }
  };
});

return出来的object即为directive,上例可见compile返回出一个postLink的fn,当然完整的应该是一个包含preLink和postLink的object,如:

代码语言:javascript复制
{
  compile: function () {
    return {
      pre: function () {},
      post: function () {}
    };
  }
}

2). 返回的linkFn进行收集,收集至preLinkFnspostLinkFns中,供后续调用

代码语言:javascript复制
addLinkFns(...)

这边有个isFunction的判断,就是如果返回的只是function,然后就当作post收集,如果是object那么根据所属字段,pre还是post

代码语言:javascript复制
if (isFunction(linkFn)) {
  addLinkFns(null, linkFn, attrStart, attrEnd);
} else if (linkFn) {
  addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
}

3). 最后返回nodeLinkFn函数

3. Link阶段

代码语言:javascript复制
compile.publicLinkFn -> compileNodes.compositeLinkFn -> applyDirectivesToNode.nodeLinkFn

3.1. publicLinkFn()

代码语言:javascript复制
function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)

1). 给每个element绑定了scope

代码语言:javascript复制
// 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

代码语言:javascript复制
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);

3.2. compositeLinkFn()

代码语言:javascript复制
function compositeLinkFn(scope, nodeList, $rootElement, parentBoundTranscludeFn)

compositeLinkFn主要任务是执行applyDirectivesToNode返回的nodeLinkFn,以及递归调用compileNodes(childNodes)返回的compositeLinkFn

代码语言:javascript复制
if (nodeLinkFn) {
  //判断directive是不是定义的scope:true,进行处理
  if (nodeLinkFn.scope) {
    childScope = scope.$new();
    $node.data('$scope', childScope);
  } else {
    childScope = scope;
  }
  
  //有关transclude的处理,后续分析
  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);
}
代码语言:javascript复制
//有段细节的地方,为什么要复制一个node数组出来呢?
//因为link阶段会对nodeList增加删除,会影响linkFn数组的执行
//复制出来数组能保证每个linkFn都会准确地执行
var nodeListLength = nodeList.length,
    stableNodeList = new Array(nodeListLength);
for (i = 0; i < nodeListLength; i  ) {
  stableNodeList[i] = nodeList[i];
}

3.3. nodeLinkFn()

nodeLinkFn是执行之前众多directive的compile后收集的prepost方法

代码语言:javascript复制
// 对scope定义中@=&的解析,生成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);
  }
})

接着以此执行controllerFns > preLinkFns > 递归childNodeLinkFn > postLinkFns

这就解释了dirtive中link,compile,ctrl顺序是 A.ctrl > A.preLink > a.ctrl > a.preLink > a.postLink > A.postLink

a是A的child-node

1)controllers执行

代码语言:javascript复制
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;
    // 当配置controller: @ 时使用attr中配置的名字
    if (controller == '@') {
      controller = attrs[directive.name];
    }

    //实例化controller
    controllerInstance = $controller(controller, locals);
    
    elementControllers[directive.name] = controllerInstance;
    if (!hasElementTranscludeDirective) {
      $element.data('$'   directive.name   'Controller', controllerInstance);
    }

    // 当配置controllerAs时将实例绑定到scope上
    if (directive.controllerAs) {
      locals.$scope[directive.controllerAs] = controllerInstance;
    }
  });
}

2) preLink 执行

代码语言:javascript复制
// 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()是用来获取directive中定义require的driective的ctrl

3) childLinkFn

代码语言:javascript复制
childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);

4) postLink

代码语言:javascript复制
// 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));
  }
}

所有linkFn (pre和post) 参数都一样

代码语言:javascript复制
function link (scope, element, attrs, ctrls, transclude);

4. transclude

4.1 transclude的定义配置

先回忆下transclude配置

代码语言:javascript复制
{
  transclude: true // or 'element'
}
  • 当配置element时,被transclude的是整个元素
  • 当配置true是,被transclude的只是该元素的子元素

4.2 transclude主要源码

又是一个调用链,最终调用入口在用户定义的link中,例如:

代码语言:javascript复制
{
  link: function (scope, el, attrs, ctrls, transclude) {
    transclude();
  }
}

那该参数是什么地方传入的?

截取nodeLinkFn中执行postLink的代码(preLink也一样,省略)

代码语言:javascript复制
linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
                linkFn.require && getControllers(linkFn.directiveName, linkFn.require, $element, elementControllers), transcludeFn);

就是最后那个参数,那么最后的那个参数到底是什么?

代码语言:javascript复制
// boundTranscludeFn 是nodeLinkFn的参数
// function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn)
// 表明当存在boundTranscludeFn时,将controllersBoundTransclude赋值给transcludeFn
transcludeFn = boundTranscludeFn && controllersBoundTransclude;


//... (省略中间代码)


// 处理了两件事:
// 1、无参数或者一个参数时,scope=undefined
// 2、将该element上的controllers赋值给第三个参数
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);
}

这么看link中传入的参数transcludeFn,其实还是nodeLinkFn的参数boundTranscludeFn,只是做了下参数处理

由上面分享可知,nodeLinkFn是在compositeLinkFn中调用,那么该参数也由此传入,代码如下

代码语言:javascript复制
// 当该element就是定义了directive并且配置了transclude
// 调用createBoundTranscludeFn生成childBoundTranscludeFn,!注意!参数传入的是nodeLinkFn.transclude
if (nodeLinkFn.transcludeOnThisElement) {
  childBoundTranscludeFn = createBoundTranscludeFn(scope, nodeLinkFn.transclude, parentBoundTranscludeFn);

} 
// 当该elementd的parent定义了transclude的directive
// 直接使用父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 就是第一if情况中的nodeLinkFn.transclude
// previousBoundTranscludeFn 就是parentBoundTranscludeFn
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {

  var boundTranscludeFn = function(transcludedScope, cloneFn, controllers) {
    var scopeCreated = false;

    // 传入scope就使用传入的参数,没有就使用当前scope.$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;
}

所以看代码知,处理了下scope,以及监听了$destroy事件进行销毁,然后就是调用传入的第二个参数transcludeFn

而transcludeFn就是nodeLinkFn.transclude,回到nodeLinkFn生成的地方--applyDirectivesToNode()

代码语言:javascript复制
// 配置 transclude:'element'时是整个元素进行compile
// 配置 transclude: true时是子元素进行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);
  
  // 递归调用compile返回publicLinkFn
  // 传入当前directive的priority,作为终止priority防止死循环
  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;

因此,childTranscludeFn其实就是compile返回的publicLinkFn,分析结论:transcludeFn其实就是调用publicLinkFn

4.3 transcludeFn的传承

当template中含有directive时如何在该子directive的link中获取到$transclude(即parent的原有childNode的publicLinkFn)来调用

在nodeLinkFn中存在以下代码

代码语言:javascript复制
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);

boundTranscludeFn是没有经过controllersBoundTransclude()包装过因为每个element的directive对应的controllers不同需要现用现调

由此传入publicLinkFn的parentBoundTranscludeFn

代码语言:javascript复制
function publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)

然后在compositeLinkFn中洗白成childBoundTranscludeFn,最终流入到link的参数$transclude供使用

代码语言:javascript复制
else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
  childBoundTranscludeFn = parentBoundTranscludeFn;
}

nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);

4.4 应用

由此延展,当定义了transclude的directive,link方法中可以调用transcludeFn来获取compile和link后的子元素,例如

代码语言:javascript复制
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>

**/

可以联想到ng-transclude

代码语言:javascript复制
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,关于cloneFn见下:

代码语言:javascript复制
var $linkNode = cloneConnectFn
  ? JQLitePrototype.clone.call($compileNodes)
  : $compileNodes;

// ...

if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
return $linkNode;
  1. 进行jq的clone
  2. 调用cloneFn

这边我有个疑问:为什么要先clone下呢?望知道的指点下,谢谢!

链接

angularjs源码笔记(1.1)--directive compile

angularjs源码笔记(1.2)--directive template

angularjs源码笔记(2)--inject

angularjs源码笔记(3)--scope

0 人点赞