Angular源码剖析之$compile

@(Angular)

$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最主要的编译,链接两个阶段,从而最先了真正意义上的双向绑定。

    原文作者:RoyalRover
    原文地址: https://segmentfault.com/a/1190000004373258
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞