Angular中$compile源码分析


Posted in Javascript onJanuary 28, 2016

$compile,在Angular中即“编译”服务,它涉及到Angular应用的“编译”和“链接”两个阶段,根据从DOM树遍历Angular的根节点(ng-app)和已构造完毕的 \$rootScope对象,依次解析根节点后代,根据多种条件查找指令,并完成每个指令相关的操作(如指令的作用域,控制器绑定以及transclude等),最终返回每个指令的链接函数,并将所有指令的链接函数合成为一个处理后的链接函数,返回给Angluar的bootstrap模块,最终启动整个应用程序。

[TOC]

Angular的compileProvider

抛开Angular的MVVM实现方式不谈,Angular给前端带来了一个软件工程的理念-依赖注入DI。依赖注入从来只是后端领域的实现机制,尤其是javaEE的spring框架。采用依赖注入的好处就是无需开发者手动创建一个对象,这减少了开发者相关的维护操作,让开发者无需关注业务逻辑相关的对象操作。那么在前端领域呢,采用依赖注入有什么与之前的开发不一样的体验呢?

我认为,前端领域的依赖注入,则大大减少了命名空间的使用,如著名的YUI框架的命名空间引用方式,在极端情况下对象的引用可能会非常长。而采用注入的方式,则消耗的仅仅是一个局部变量,好处自然可见。而且开发者仅仅需要相关的“服务”对象的名称,而不需要知道该服务的具体引用方式,这样开发者就完全集中在了对象的借口引用上,专注于业务逻辑的开发,避免了反复的查找相关的文档。

前面废话一大堆,主要还是为后面的介绍做铺垫。在Angular中,依赖注入对象的方式依赖与该对象的Provider,正如小结标题的compileProvider一样,该对象提供了compile服务,可通过injector.invoke(compileProvider.$get,compileProvider)函数完成compile服务的获取。因此,问题转移到分析compileProvider.\$get的具体实现上。

compileProvider.\$get

this.\$get = ['\$injector', '\$parse', '\$controller', '\$rootScope', '\$http', '\$interpolate',
   function(\$injector, \$parse, \$controller, \$rootScope, \$http, \$interpolate) {
 ...
 return compile;
}

上述代码采用了依赖注入的方式注入了\$injector,\$parse,\$controller,\$rootScope,\$http,\$interpolate五个服务,分别用于实现“依赖注入的注入器(\$injector),js代码解析器(\$parse),控制器服务(\$controller),根作用域(\$rootScope),http服务和指令解析服务”。compileProvider通过这几个服务单例,完成了从抽象语法树的解析到DOM树构建,作用域绑定并最终返回合成的链接函数,实现了Angular应用的开启。

\$get方法最终返回compile函数,compile函数就是\$compile服务的具体实现。下面我们深入compile函数:

function compile(\$compileNodes, maxPriority) {
   var compositeLinkFn = compileNodes(\$compileNodes, maxPriority);

   return function publicLinkFn(scope, cloneAttachFn, options) {
    options = options || {};
    var parentBoundTranscludeFn = options.parentBoundTranscludeFn;
    var transcludeControllers = options.transcludeControllers;
    if (parentBoundTranscludeFn && parentBoundTranscludeFn.$$boundTransclude) {
     parentBoundTranscludeFn = parentBoundTranscludeFn.$$boundTransclude;
    }
    var $linkNodes;
    if (cloneAttachFn) {
     $linkNodes = $compileNodes.clone();
     cloneAttachFn($linkNodes, scope);
    } else {
     $linkNodes = $compileNodes;
    }
    _.forEach(transcludeControllers, function(controller, name) {
     $linkNodes.data('$' + name + 'Controller', controller.instance);
    });
    $linkNodes.data('$scope', scope);
    compositeLinkFn(scope, $linkNodes, parentBoundTranscludeFn);
    return $linkNodes;
   };
  }

首先,通过compileNodes函数,针对所需要遍历的根节点开始,完成指令的解析,并生成合成之后的链接函数,返回一个publicLinkFn函数,该函数完成根节点与根作用域的绑定,并在根节点缓存指令的控制器实例,最终执行合成链接函数。

合成链接函数的生成

通过上一小结,可以看出\$compile服务的核心在于compileNodes函数的执行及其返回的合成链接函数的执行。下面,我们深入到compileNodes的具体逻辑中去:

function compileNodes($compileNodes, maxPriority) {
   var linkFns = [];
   _.times($compileNodes.length, function(i) {
    var attrs = new Attributes($($compileNodes[i]));
    var directives = collectDirectives($compileNodes[i], attrs, maxPriority);
    var nodeLinkFn;
    if (directives.length) {
     nodeLinkFn = applyDirectivesToNode(directives, $compileNodes[i], attrs);
    }
    var childLinkFn;
    if ((!nodeLinkFn || !nodeLinkFn.terminal) &&
      $compileNodes[i].childNodes && $compileNodes[i].childNodes.length) {
     childLinkFn = compileNodes($compileNodes[i].childNodes);
    }
    if (nodeLinkFn && nodeLinkFn.scope) {
     attrs.$$element.addClass('ng-scope');
    }
    if (nodeLinkFn || childLinkFn) {
     linkFns.push({
      nodeLinkFn: nodeLinkFn,
      childLinkFn: childLinkFn,
      idx: i
     });
    }
   });

   // 执行指令的链接函数
   function compositeLinkFn(scope, linkNodes, parentBoundTranscludeFn) {
    var stableNodeList = [];
    _.forEach(linkFns, function(linkFn) {
     var nodeIdx = linkFn.idx;
     stableNodeList[linkFn.idx] = linkNodes[linkFn.idx];
    });

    _.forEach(linkFns, function(linkFn) {
     var node = stableNodeList[linkFn.idx];
     if (linkFn.nodeLinkFn) {
      var childScope;
      if (linkFn.nodeLinkFn.scope) {
       childScope = scope.$new();
       $(node).data('$scope', childScope);
      } else {
       childScope = scope;
      }

      var boundTranscludeFn;
      if (linkFn.nodeLinkFn.transcludeOnThisElement) {
       boundTranscludeFn = function(transcludedScope, cloneAttachFn, transcludeControllers, containingScope) {
        if (!transcludedScope) {
         transcludedScope = scope.$new(false, containingScope);
        }
        var didTransclude = linkFn.nodeLinkFn.transclude(transcludedScope, cloneAttachFn, {
         transcludeControllers: transcludeControllers,
         parentBoundTranscludeFn: parentBoundTranscludeFn
        });
        if (didTransclude.length === 0 && parentBoundTranscludeFn) {
         didTransclude = parentBoundTranscludeFn(transcludedScope, cloneAttachFn);
        }
        return didTransclude;
       };
      } else if (parentBoundTranscludeFn) {
       boundTranscludeFn = parentBoundTranscludeFn;
      }

      linkFn.nodeLinkFn(
       linkFn.childLinkFn,
       childScope,
       node,
       boundTranscludeFn
      );
     } else {
      linkFn.childLinkFn(
       scope,
       node.childNodes,
       parentBoundTranscludeFn
      );
     }
    });
   }

   return compositeLinkFn;
  }

代码有些长,我们一点一点分析。

首先,linkFns数组用于存储每个DOM节点上所有指令的处理后的链接函数和子节点上所有指令的处理后的链接函数,具体使用递归的方式实现。随后,在返回的compositeLinkFn中,则是遍历linkFns,针对每个链接函数,创建起对应的作用域对象(针对创建隔离作用域的指令,创建隔离作用域对象,并保存在节点的缓存中),并处理指令是否设置了transclude属性,生成相关的transclude处理函数,最终执行链接函数;如果当前指令并没有链接函数,则调用其子元素的链接函数,完成当前元素的处理。

在具体的实现中,通过collectDirectives函数完成所有节点的指令扫描。它会根据节点的类型(元素节点,注释节点和文本节点)分别按特定规则处理,对于元素节点,默认存储当前元素的标签名为一个指令,同时扫描元素的属性和CSS class名,判断是否满足指令定义。

紧接着,执行applyDirectivesToNode函数,执行指令相关操作,并返回处理后的链接函数。由此可见,applyDirectivesToNode则是\$compile服务的核心,重中之重!

applyDirectivesToNode函数

applyDirectivesToNode函数过于复杂,因此只通过简单代码说明问题。
上文也提到,在该函数中执行用户定义指令的相关操作。

首先则是初始化相关属性,通过遍历节点的所有指令,针对每个指令,依次判断$$start属性,优先级,隔离作用域,控制器,transclude属性判断并编译其模板,构建元素的DOM结构,最终执行用户定义的compile函数,将生成的链接函数添加到preLinkFns和postLinkFns数组中,最终根据指令的terminal属性判断是否递归其子元素指令,完成相同的操作。

其中,针对指令的transclude处理则需特殊说明:

if (directive.transclude === 'element') {
      hasElementTranscludeDirective = true;
      var $originalCompileNode = $compileNode;
      $compileNode = attrs.$$element = $(document.createComment(' ' + directive.name + ': ' + attrs[directive.name] + ' '));
      $originalCompileNode.replaceWith($compileNode);
      terminalPriority = directive.priority;
      childTranscludeFn = compile($originalCompileNode, terminalPriority);
     } else {
      var $transcludedNodes = $compileNode.clone().contents();
      childTranscludeFn = compile($transcludedNodes);
      $compileNode.empty();
     }

如果指令的transclude属性设置为字符串“element”时,则会用注释comment替换当前元素节点,再重新编译原先的DOM节点,而如果transclude设置为默认的true时,则会继续编译其子节点,并通过transcludeFn传递编译后的DOM对象,完成用户自定义的DOM处理。

在返回的nodeLinkFn中,根据用户指令的定义,如果指令带有隔离作用域,则创建一个隔离作用域,并在当前的dom节点上绑定ng-isolate-scope类名,同时将隔离作用域缓存到dom节点上;

接下来,如果dom节点上某个指令定义了控制器,则会调用\$cotroller服务,通过依赖注入的方式(\$injector.invoke)获取该控制器的实例,并缓存该控制器实例;
随后,调用initializeDirectiveBindings,完成隔离作用域属性的单向绑定(@),双向绑定(=)和函数的引用(&),针对隔离作用域的双向绑定模式(=)的实现,则是通过自定义的编译器完成简单Angular语法的编译,在指定作用域下获取表达式(标示符)的值,保存为lastValue,并通过设置parentValueFunction添加到当前作用域的$watch数组中,每次\$digest循环,判断双向绑定的属性是否变脏(dirty),完成值的同步。

最后,根据applyDirectivesToNode第一步的初始化操作,将遍历执行指令compile函数返回的链接函数构造出成的preLinkFns和postLinkFns数组,依次执行,如下所示:

_.forEach(preLinkFns, function(linkFn) {
     linkFn(
      linkFn.isolateScope ? isolateScope : scope,
      $element,
      attrs,
      linkFn.require && getControllers(linkFn.require, $element),
      scopeBoundTranscludeFn
     );
    });
    if (childLinkFn) {
     var scopeToChild = scope;
     if (newIsolateScopeDirective && newIsolateScopeDirective.template) {
      scopeToChild = isolateScope;
     }
     childLinkFn(scopeToChild, linkNode.childNodes, boundTranscludeFn);
    }
    _.forEachRight(postLinkFns, function(linkFn) {
     linkFn(
      linkFn.isolateScope ? isolateScope : scope,
      $element,
      attrs,
      linkFn.require && getControllers(linkFn.require, $element),
      scopeBoundTranscludeFn
     );
    });

可以看出,首先执行preLinkFns的函数;紧接着遍历子节点的链接函数,并执行;最后执行postLinkFns的函数,完成当前dom元素的链接函数的执行。指令的compile函数默认返回postLink函数,可以通过compile函数返回一个包含preLink和postLink函数的对象设置preLinkFns和postLinkFns数组,如在preLink针对子元素进行DOM操作,效率会远远高于在postLink中执行,原因在于preLink函数执行时并未构建子元素的DOM,在当子元素是个拥有多个项的li时尤为明显。

end of compile-publicLinkFn

终于,到了快结束的阶段了。通过compileNodes返回从根节点(ng-app所在节点)开始的所有指令的最终合成链接函数,最终在publicLinkFn函数中执行。在publicLinkFn中,完成根节点与根作用域的绑定,并在根节点缓存指令的控制器实例,最终执行合成链接函数,完成了Angular最重要的编译,链接两个阶段,从而开始了真正意义上的双向绑定。

Javascript 相关文章推荐
JavaScript 上万关键字瞬间匹配实现代码
Jul 07 Javascript
jQuery显示和隐藏 常用的状态判断方法
Jan 29 Javascript
JS作为值的函数用法示例
Jun 20 Javascript
JavaScript 限制文本框不可输入英文单双引号的方法
Dec 20 Javascript
easyui 中的datagrid跨页勾选问题的实现方法
Jan 18 Javascript
input file样式修改以及图片预览删除功能详细概括(推荐)
Aug 17 Javascript
JS实现读取xml内容并输出到div中的方法示例
Apr 19 Javascript
JavaScript中七种流行的开源机器学习框架
Oct 11 Javascript
JS实现电话号码的字母组合算法示例
Feb 26 Javascript
angular2 NgModel模块的具体使用方法
Apr 10 Javascript
微信小程序实现左滑动删除效果
Mar 30 Javascript
vue 实现input表单元素的disabled示例
Oct 28 Javascript
实例讲解JS中setTimeout()的用法
Jan 28 #Javascript
jQuery+canvas实现的球体平抛及颜色动态变换效果
Jan 28 #Javascript
jQuery+canvas实现简单的球体斜抛及颜色动态变换效果
Jan 28 #Javascript
基于javascript实现动态显示当前系统时间
Jan 28 #Javascript
jQuery实现div随意拖动的实例代码(通用代码)
Jan 28 #Javascript
jQuery+css实现炫目的动态块漂移效果
Jan 28 #Javascript
使用node+vue.js实现SPA应用
Jan 28 #Javascript
You might like
1亿条数据如何分表100张到Mysql数据库中(PHP)
2015/07/29 PHP
joomla实现注册用户添加新字段的方法
2016/05/05 PHP
jquery+thinkphp实现跨域抓取数据的方法
2016/10/15 PHP
php利用嵌套数组拼接与解析json的方法
2017/02/07 PHP
PHP从零开始打造自己的MVC框架之类的自动加载实现方法详解
2019/06/03 PHP
PHP正则表达式函数preg_replace用法实例分析
2020/06/04 PHP
Opacity.js
2007/01/22 Javascript
JS类库Bindows1.3中的内存释放方式分析
2007/03/08 Javascript
Document 对象的常用方法
2009/07/31 Javascript
JS实现点击颜色块切换指定区域背景颜色的方法
2015/02/25 Javascript
jQuery时间轴插件使用详解
2015/07/16 Javascript
jquery 遍历数组 each 方法详解
2016/05/25 Javascript
JavaScript中的对象和原型(一)
2016/08/12 Javascript
浅谈Node.js 子进程与应用场景
2018/01/24 Javascript
ionic grid(栅格)九宫格制作详解
2018/06/30 Javascript
在微信小程序里使用watch和computed的方法
2018/08/02 Javascript
angularJs复选框checkbox选中进行ng-show显示隐藏的方法
2018/10/08 Javascript
JS中FileReader类实现文件上传及时预览功能
2020/03/27 Javascript
JavaScript设计模式--简单工厂模式定义与应用案例详解
2020/05/23 Javascript
Python通过websocket与js客户端通信示例分析
2014/06/25 Python
python统计多维数组的行数和列数实例
2018/06/23 Python
python3使用matplotlib绘制条形图
2020/03/25 Python
在Django admin中编辑ManyToManyField的实现方法
2019/08/09 Python
flask框架json数据的拿取和返回操作示例
2019/11/28 Python
python利用datetime模块计算程序运行时间问题
2020/02/20 Python
在Python中实现字典反转案例
2020/12/05 Python
OnePlus加拿大官网:中国国际化手机品牌
2020/10/13 全球购物
会计电算化专业应届大学生求职信
2013/10/22 职场文书
毕业生的自我评价
2013/12/30 职场文书
经典演讲稿范文
2013/12/30 职场文书
大学学生会竞选演讲稿
2014/04/25 职场文书
竞聘演讲稿精彩开头和结尾
2014/05/14 职场文书
民族学专业大学生职业规划范文:清晰未来的构想
2014/09/20 职场文书
vue2实现provide inject传递响应式
2021/05/21 Vue.js
JPA如何使用entityManager执行SQL并指定返回类型
2021/06/15 Java/Android
详解java如何集成swagger组件
2021/06/21 Java/Android