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 相关文章推荐
jQuery 使用手册(七)
Sep 23 Javascript
JS模块与命名空间的介绍
Mar 22 Javascript
用jQuery模拟select下拉框的简单示例代码
Jan 26 Javascript
js中top的作用深入剖析
Mar 04 Javascript
jQuery实现左侧导航模块的显示与隐藏效果
Jul 04 Javascript
微信小程序 安全包括(框架、功能模块、账户使用)详解
Jan 16 Javascript
jQuery无刷新上传之uploadify简单代码
Jan 17 Javascript
vue使用watch 观察路由变化,重新获取内容
Mar 08 Javascript
Vue 菜单栏点击切换单个class(高亮)的方法
Aug 22 Javascript
详解基于mpvue微信小程序下载远程图片到本地解决思路
May 16 Javascript
基于javascript处理二进制图片流过程详解
Jun 08 Javascript
浅析微信小程序自定义日历组件及flex布局最后一行对齐问题
Oct 29 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
php5.3 goto函数介绍和示例
2014/03/21 PHP
ThinkPHP模板输出display用法分析
2014/11/26 PHP
分享一个Laravel好用的Cache宏
2015/03/02 PHP
php过滤输入操作之htmlentities与htmlspecialchars用法分析
2017/02/17 PHP
Yii2 加载css、js 载静态资源的方法
2017/03/10 PHP
Thinkphp 框架扩展之类库扩展操作详解
2020/04/23 PHP
解决extjs在firefox中关闭窗口再打开后iframe中js函数访问不到的问题
2008/11/06 Javascript
关于javascript document.createDocumentFragment()
2009/04/04 Javascript
Js与下拉列表处理问题解决
2014/02/13 Javascript
js(JavaScript)实现TAB标签切换效果的简单实例
2014/02/26 Javascript
为jquery的ajaxfileupload增加附加参数的方法
2014/03/04 Javascript
防止登录页面出现在frame中js代码
2014/07/22 Javascript
浅谈js中test()函数在正则中的使用
2016/08/19 Javascript
jQuery webuploader分片上传大文件
2016/11/07 Javascript
canvas 实现中国象棋
2017/02/17 Javascript
微信小程序 flex实现导航实例详解
2017/04/26 Javascript
js实现一个简单的MVVM框架示例
2018/01/15 Javascript
Vue项目实现简单的权限控制管理功能
2019/07/17 Javascript
[02:32]DOTA2英雄基础教程 祸乱之源
2013/12/23 DOTA
用map函数来完成Python并行任务的简单示例
2015/04/02 Python
Python中列表元素转为数字的方法分析
2016/06/14 Python
python3新特性函数注释Function Annotations用法分析
2016/07/28 Python
Python字符串逆序输出的实例讲解
2019/02/16 Python
python使用for...else跳出双层嵌套循环的方法实例
2020/05/17 Python
Python如何急速下载第三方库详解
2020/11/02 Python
家居设计专业个人自荐信范文
2013/11/26 职场文书
教师个人的自我评价分享
2014/01/02 职场文书
会计工作心得体会
2014/01/13 职场文书
初中英语课后反思
2014/04/25 职场文书
党建工作汇报材料
2014/12/24 职场文书
承诺保证书格式
2015/02/28 职场文书
黑白记忆观后感
2015/06/18 职场文书
2016元旦主持人开场白
2015/12/03 职场文书
Vue.js 带下拉选项的输入框(Textbox with Dropdown)组件
2021/04/17 Vue.js
Python 类,对象,数据分类,函数参数传递详解
2021/09/25 Python
nginx容器方式反向代理实战
2022/04/18 Servers