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

Compile (1)

1. 结构

$compile跟其他service一样都需注册一个provider--$CompileProvider就是compile注册进angular的provider。这样$compile可以作为service被注入到其他方法的参数中。

主要的调用路径如下:

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阶段调用
// 将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];
  }
});
var compositeLinkFn = compileNodes($compileNodes,transcludeFn,$compileNodes,maxPriority,ignoreDirective,previousCompileContext);
return function publicLinkFn(scope,cloneConnectFn,transcludeControllers,parentBoundTranscludeFn)

2.2. compileNodes()

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

1). 收集directives

directives = collectDirectives(nodeList[i]....);

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

nodeLinkFn = applyDirectivesToNode(directives,nodeList[i]....)

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

childLinkFn = compileNodes(childNodes...)

4). 返回compositeLinkFn

2.3.applyDirectivesToNode()

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

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

linkFn = directive.compile($compileNode,templateAttrs,childTranscludeFn);

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

module.directive('xxx',function () {
  return {
    compile: function () {
      return function postLinkFn() {};
    }
  };
});

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

{
  compile: function () {
    return {
      pre: function () {},post: function () {}
    };
  }
}

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

addLinkFns(...)

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

if (isFunction(linkFn)) {
  addLinkFns(null,linkFn,attrStart,attrEnd);
} else if (linkFn) {
  addLinkFns(linkFn.pre,linkFn.post,attrEnd);
}

3). 最后返回nodeLinkFn函数

3. Link阶段

compile.publicLinkFn -> compileNodes.compositeLinkFn -> applyDirectivesToNode.nodeLinkFn

3.1.publicLinkFn()

function publicLinkFn(scope,parentBoundTranscludeFn)

1). 给每个element绑定了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

if (compositeLinkFn) compositeLinkFn(scope,$linkNode,parentBoundTranscludeFn);

3.2.compositeLinkFn()

function compositeLinkFn(scope,nodeList,$rootElement,parentBoundTranscludeFn)

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

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,childBoundTranscludeFn);

} else if (childLinkFn) {
  //childLinkFn === compositeLinkFn
  childLinkFn(scope,node.childNodes,undefined,parentBoundTranscludeFn);
}
//有段细节的地方,为什么要复制一个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方法

// 对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执行

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 执行

// 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,elementControllers),transcludeFn);
  } catch (e) {
    $exceptionHandler(e,startingTag($element));
  }
}

getControllers()是用来获取directive中定义require的driective的ctrl

3) childLinkFn

childLinkFn(scopeToChild,linkNode.childNodes,boundTranscludeFn);

4) postLink

// POSTLINKING
for(i = postLinkFns.length - 1; i >= 0; i--) {
  try {
    linkFn = postLinkFns[i];
    linkFn(linkFn.isolateScope ? isolateScope : scope,startingTag($element));
  }
}

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

function link (scope,element,ctrls,transclude);

4. transclude

4.1 transclude的定义配置

先回忆下transclude配置

{
  transclude: true // or 'element'
}
  • 当配置element时,被transclude的是整个元素
  • 当配置true是,被transclude的只是该元素的子元素

4.2 transclude主要源码

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

{
  link: function (scope,el,transclude) {
    transclude();
  }
}

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

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

linkFn(linkFn.isolateScope ? isolateScope : scope,transcludeFn);

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

// boundTranscludeFn 是nodeLinkFn的参数
// function nodeLinkFn(childLinkFn,scope,linkNode,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中调用,那么该参数也由此传入,代码如下

// 当该element就是定义了directive并且配置了transclude
// 调用createBoundTranscludeFn生成childBoundTranscludeFn,!注意!参数传入的是nodeLinkFn.transclude
if (nodeLinkFn.transcludeOnThisElement) {
  childBoundTranscludeFn = createBoundTranscludeFn(scope,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,childBoundTranscludeFn);

// ...

// transcludeFn 就是第一if情况中的nodeLinkFn.transclude
// previousBoundTranscludeFn 就是parentBoundTranscludeFn
function createBoundTranscludeFn(scope,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,controllers,previousBoundTranscludeFn);
    if (scopeCreated) {
      clone.on('$destroy',function() { transcludedScope.$destroy(); });
    }
    return clone;
  };

  return boundTranscludeFn;
}

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

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

// 配置 transclude:'element'时是整个元素进行compile
// 配置 transclude: true时是子元素进行compile
if (directiveValue == 'element') {
  hasElementTranscludeDirective = true;
  terminalPriority = directive.priority;
  $template = groupScan(compileNode,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,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中存在以下代码

childLinkFn && childLinkFn(scopeToChild,boundTranscludeFn);

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

由此传入publicLinkFn的parentBoundTranscludeFn

function publicLinkFn(scope,parentBoundTranscludeFn)

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

else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
  childBoundTranscludeFn = parentBoundTranscludeFn;
}

nodeLinkFn(childLinkFn,childBoundTranscludeFn);

4.4 应用

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

directive('myDir',function () {
  return {
    transclude: true,replace: true,template: '<div class="my-dir"></div>'
    link: function (scope,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

var ngTranscludeDirective = ngDirective({
  link: function($scope,$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见下:

var $linkNode = cloneConnectFn
  ? JQLitePrototype.clone.call($compileNodes)
  : $compileNodes;

// ...

if (cloneConnectFn) cloneConnectFn($linkNode,scope);
if (compositeLinkFn) compositeLinkFn(scope,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

相关文章

ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中...
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是...
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,...
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:h...
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目...
Angular数据更新不及时问题探讨前言 在修复控制角标正确变...