Angular源碼分析之$compile

@(Angular)javascript

$compile,在Angular中即「編譯」服務,它涉及到Angular應用的「編譯」和「連接」兩個階段,根據從DOM樹遍歷Angular的根節點(ng-app)和已構造完畢的 $rootScope對象,依次解析根節點後代,根據多種條件查找指令,並完成每一個指令相關的操做(如指令的做用域,控制器綁定以及transclude等),最終返回每一個指令的連接函數,並將全部指令的連接函數合成爲一個處理後的連接函數,返回給Angluar的bootstrap模塊,最終啓動整個應用程序。前端


[TOC]java

Angular的compileProvider

拋開Angular的MVVM實現方式不談,Angular給前端帶來了一個軟件工程的理念-依賴注入DI。依賴注入歷來只是後端領域的實現機制,尤爲是javaEE的spring框架。採用依賴注入的好處就是無需開發者手動建立一個對象,這減小了開發者相關的維護操做,讓開發者無需關注業務邏輯相關的對象操做。那麼在前端領域呢,採用依賴注入有什麼與以前的開發不同的體驗呢?node

我認爲,前端領域的依賴注入,則大大減小了命名空間的使用,如著名的YUI框架的命名空間引用方式,在極端狀況下對象的引用可能會很是長。而採用注入的方式,則消耗的僅僅是一個局部變量,好處天然可見。並且開發者僅僅須要相關的「服務」對象的名稱,而不須要知道該服務的具體引用方式,這樣開發者就徹底集中在了對象的接口引用上,專一於業務邏輯的開發,避免了反覆的查找相關的文檔。spring

前面廢話一大堆,主要仍是爲後面的介紹作鋪墊。在Angular中,依賴注入對象的方式依賴與該對象的Provider,正如小結標題的compileProvider同樣,該對象提供了compile服務,可經過injector.invoke(compileProvider.$get,compileProvider)函數完成compile服務的獲取。所以,問題轉移到分析compileProvider.$get的具體實現上。bootstrap

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的具體邏輯中去:app

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最重要的編譯,連接兩個階段,從而開始了真正意義上的雙向綁定。

相關文章
相關標籤/搜索