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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中文文档更容易理解,前提是你的英语基础还可以。英文文档对于知识点讲述简明扼要,通俗易懂,而有些中文文档读起来特别费力,基础差、底子薄的有可能一会就会被绕晕了,最起码英文文档中的代码与中文文档中的代码是一致的,但知识点讲述实在是差距太大。Angular.jshasapowerfuldire
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上。目前,它支持6种图表类型(折线图,条形图,雷达图,饼图,柱状图和极地区域区)。而且,这是一个独立的包,不依赖第三方 JavaScript 库,小于 5KB。   其中用到的软件:   Chart.js框架,版本1.0.2,一
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,结局方案为更换jquery、angularjs、IE的版本。 1.首先尝试更换jquery版本为1.7.2 jquery-1.9.1.js-->jquery-1.7.2.js--> jquery2.1.4.js 无效 2.尝试更换IE版本IE8 IE11-
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:http://ngmodules.org/modules/angularjs-dropdown-multiselecthttp://dotansimha.github.io/angularjs-dropdown-multiselect/#/AngularJSDropdownMultiselectThisdire
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目需求,需要在首页搜索框中添加语音输入功能,考虑到科大讯飞语音业务的强大能力,遂决定使用科大讯飞语音输入第三方服务。软件首页截图如下所示: 涉及的源代码如下所示: //语音识别$rootScope.startRecognize = function() {var speech;
Angular数据更新不及时问题探讨前言 在修复控制角标正确变化过程中,发觉前端代码组织层次出现了严重问题。传递和共享数据时自己使用的是rootScope,为此造成了全局变量空间的污染。根据《AngularJs深度剖析与最佳实践》,如果两个控制器的协作存在大量的数据共享和交互可以利用Factory等服务的“单例”特性为它们注入一个共享对象来传递数据。而自己在使用rootScope
HTML:让表单、文本框只读,不可编辑的方法有时候,我们希望表单中的文本框是只读的,让用户不能修改其中的信息,如使中国">的内容,"中国"两个字不可以修改。实现的方式归纳一下,有如下几种。方法1:onfocus=this.blur()中国"onfocus=this.blur()>方法2:readonly中国"readonly>中国"readonly="tru
在AngularJS应用中实现微信认证授权遇到的坑前言 项目开发过程中,移动端新近增加了一个功能“微信授权登录”,由于自己不是负责移动端开发的,但最后他人负责的部分未达到预期效果。不能准确实现微信授权登录。最后还得靠自己做进一步的优化工作,谁让自己是负责人呢?原来负责人就是负责最后把所有的BUG解决掉。 首先,熟悉一下微信授权部分的源代码,如下所示:
AngularJS实现二维码信息的集成思路需求 实现生成的二维码包含订单详情信息。思路获取的内容数据如下: 现在可以获取到第一级数据,第二级数据data获取不到。利用第一级数据的获取方法获取不到第二级数据。for(i in data){alert(i); //获得属性 if(typeof(data[i]) == "o
Cookie'data'possiblynotsetoroverflowedbecauseitwastoolarge(5287>4096bytes)!故事起源 项目开发过程中遇到以上问题,刚开始以为只是个警告,没太在意。后来发现直接影响到了程序的执行效果。果断寻找解决方法。问题分析 根据Chrome浏览器信息定位,显示以下代码存在错误:
AngularJS控制器controller之间如何通信angular控制器通信的方式有三种:1,利用作用域继承的方式。即子控制器继承父控制器中的内容2,基于事件的方式。即$on,$emit,$boardcast这三种方式3,服务方式。写一个服务的单例然后通过注入来使用利用作用域的继承方式由于作用域的继承是基于js的原型继承方式,所以这里分为两种情况,当作用域上面的值
AngularJS路由问题解决遇到了一个棘手的问题:点击优惠详情时总是跳转到药店详情页面中去。再加一层地址解决了,但是后来发现问题还是来了:Couldnotresolve'yhDtlMaintain/yhdetail'fromstate'yhMaintain'药店详情http://192.168.1.118:8088/lmapp/index.html#/0优惠券详情
书海拾贝之特殊的ng-src和ng-href在说明这两个指令的特殊之前,需要先了解一下ng的启动及执行过程,如下:1)浏览器加载静态HTML文件并解析为DOM;2)浏览器加载angular.js文件;3)angular监听DOMContentLoaded事件,监听到时开始启动;4)angular寻找ng-app指令,确定作用范围;
angularjs实现页面跳转并进行参数传递Angular页面传参有多种办法,我在此列举4种最常见的:1.基于ui-router的页面跳转传参(1)在AngularJS的app.js中用ui-router定义路由,比如现在有两个页面,一个页面(producers.html)放置了多个producers,点击其中一个目标,页面跳转到对应的producer页,同时将producerId
AngularJS实现表格数据的编辑,更新和删除效果实现首先,我们先建立一些数据,当然你可以从你任何地方读出你的数据var app = angular.module('plunker', ['ui.bootstrap']);app.controller('MainCtrl', function($scope) { $scope.name = 'World'; $sc
ANGULAR三宗罪之版本陷阱      坑!碰到个大坑,前面由于绑定日期时将angular版本换为angular-1.3.0-beta.1时,后来午睡后,登录系统,发现无论如何都登陆不进去了,经过调试,发现数据视图已经无法实现双向绑定了。自己还以为又碰到了“僵尸程序”了呢,对比药店端的程序发现并没有什么不同之处。后来自己经过一番思索才隐约感觉到是不是angular的版本造成的,将版本换为之前
JS实现分页操作前言 项目开发过程中,进行查询操作时有可能会检索出大量的满足条件的查询结果。在一页中显示全部查询结果会降低用户的体验感,故需要实现分页显示效果。受前面“JS实现时间选择插件”的启发,自己首先需要查看一下HTML5能否实现此效果。 整了半天,不管是用纯CSS3也好,还是用tmpagination.js还是bootstrap组件也好,到最后自己静下心来理
浏览器兼容性解决之道前言 浏览器兼容性一直是前端开发中不得不面对的一个问题。而最突出的就是IE。对绝大多数公司来说,兼容IE6的性价比已经很低,而IE7则几乎已经绝迹。所以,常见的兼容性下限是IE8。这也正是Angular1.2x的兼容性目标,Angular团队声明:Angular的持续集成服务器会在IE8下运行所有的测试。但这些测试不会运行在IE7及以下版本,它们也不会保证An
JS利用正则表达式校验手机号绪 由于项目需求,需要在前端实现手机号码的校验。当然了,对于基本的格式校验应该放在客户端进行,而不需要再将待校验的手机号发送至服务端,在服务端完成校验,然后将校验结果返回给客户端,客户端根据返回的结果再进行进一步的处理。如此反而复杂化了处理过程。 其实,处于安全考虑,应该在服务端进行二次校验。以下为在客户端的JS中校验手机号码格式
基于项目实例解析ng启动加载过程前言 在AngularJS项目开发过程中,自己将遇到的问题进行了整理。回过头来总结一下angular的启动过程。 下面以实际项目为例进行简要讲解。1.载入ng库2.等待,直到DOM树构造完毕。3.发现ng-app,自动进入启动引导阶段。4.根据ng-app名称找到相应的路由。5.加载默认地址。6.Js顺序执行,加载相应模版页sys_tpls/