1. 程式人生 > >AngularJS 原始碼分析4

AngularJS 原始碼分析4

angularjs之$compile

今天主要說說ng裡的$compile,這是一個非常關鍵的服務,頁面上的雙向繫結,各個監聽基本上都是在這裡執行的.
原始碼部分還是引用angular1.2.4,連結在這裡下載

compile的源頭

ng裡最開始引用$compile的地方就是把所有系統內建的指令新增到$CompileProvider裡,由於程式碼太長,只寫些關鍵部分的

$provide.provider('$compile', $CompileProvider).
        directive({
            a: htmlAnchorDirective,
            input: inputDirective,
            textarea: inputDirective,
            form: formDirective,
            script: scriptDirective,
            select
: selectDirective, style: styleDirective, option: optionDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, ngBindTemplate: ngBindTemplateDirective, ngClass: ngClassDirective, ngClassEven: ngClassEvenDirective, ngClassOdd: ngClassOddDirective, ngCloak: ngCloakDirective, ngController: ngControllerDirective, ngForm: ngFormDirective, ngHide: ngHideDirective, ngIf: ngIfDirective, ngInclude: ngIncludeDirective, ngInit: ngInitDirective, ngNonBindable: ngNonBindableDirective, ngPluralize: ngPluralizeDirective, ngRepeat: ngRepeatDirective, ngShow: ngShowDirective, ngStyle: ngStyleDirective, ngSwitch: ngSwitchDirective, ngSwitchWhen: ngSwitchWhenDirective, ngSwitchDefault: ngSwitchDefaultDirective, ngOptions: ngOptionsDirective, ngTransclude: ngTranscludeDirective, ngModel: ngModelDirective, ngList: ngListDirective, ngChange: ngChangeDirective, required: requiredDirective, ngRequired: requiredDirective, ngValue: ngValueDirective }). directive({ ngInclude: ngIncludeFillContentDirective }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives);

此處的directive方法就是$CompileProvider裡的registerDirective方法,主要就是把內建指令新增到內部的hasDirectives物件內,以方便後面在全域性查詢指令的時候進行匹配.

compile的啟動

啟動的方法在這裡,只摘取關鍵程式碼.

injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate',
       function(scope, element, compile, injector, animate) {
        scope.$apply
(function() { element.data('$injector', injector); compile(element)(scope); }); }] );

上面的程式碼主要作用就是,初始化相關的依賴,然後執行全域性編譯,最後更新所有的$watch.

核心的程式碼就這一句

compile(element)(scope);

其實這裡有兩步

  • compile(element) 收集完整個頁面內的指令,然後返回publicLinkFn函式

  • 執行publicLinkFn(scope) 此處的scope即為$rootScope

先來說說第一步

compile(element)

compile服務返回的是一個建構函式,名為compile,程式碼在這裡

function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,previousCompileContext) {
        if (!($compileNodes instanceof jqLite)) {
          $compileNodes = jqLite($compileNodes);
        }
        forEach($compileNodes, function(node, index){
          if (node.nodeType == 3 && node.nodeValue.match(/\S+/)  ) {
            $compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0];
          }
        });
        var compositeLinkFn =
                compileNodes($compileNodes, transcludeFn, $compileNodes,
                             maxPriority, ignoreDirective, previousCompileContext);
        safeAddClass($compileNodes, 'ng-scope');
        return function publicLinkFn(scope, cloneConnectFn, transcludeControllers){
            // 程式碼太長,省略
        };
    }

從上面的程式碼可以看出,如果要查詢的節點是文字元素,則包裝一個span標籤,然後執行compileNodes,這個方法主要是收集指令.

function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,previousCompileContext) {
          var linkFns = [],
              attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound;

          for (var i = 0; i < nodeList.length; i++) {
                attrs = new Attributes();
                directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,ignoreDirective);

                nodeLinkFn = (directives.length)
                    ? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement,null, [], [], previousCompileContext)
                    : null;

                if (nodeLinkFn && nodeLinkFn.scope) {
                  safeAddClass(jqLite(nodeList[i]), 'ng-scope');
                }

                childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
                              !(childNodes = nodeList[i].childNodes) ||
                              !childNodes.length)
                    ? null
                    : compileNodes(childNodes,
                         nodeLinkFn ? nodeLinkFn.transclude : transcludeFn);

                linkFns.push(nodeLinkFn, childLinkFn);
                linkFnFound = linkFnFound || nodeLinkFn || childLinkFn;
                //use the previous context only for the first element in the virtual group
                previousCompileContext = null;
          }
          return linkFnFound ? compositeLinkFn : null;
          function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) {
                // 程式碼省略
           }
    }

上面的編譯節點的主要流程就是,先通過collectDirectives蒐集當前節點的指令,然後找到了,則呼叫applyDirectivesToNode來應用指令,然後查詢當前節點的子節點是否有指令,這是一個遞迴,最後把所有的函式新增到一個內部的linkFns陣列中,這個將在最後連結的時候會用到.

先來看看collectDirectives方法,這個方法程式碼比較長,就不貼了,直接說程式碼邏輯

ng收集指令的時候,首先根據節點型別

  • element_node 1 先根據tagName來新增指令,然後loop節點的attrs來新增指令,最後通過className來新增指令

  • text_node 3 假如是文字節點的話,則呼叫addTextInterpolateDirective方法來構建指令

function addTextInterpolateDirective(directives, text) {
      var interpolateFn = $interpolate(text, true);
      if (interpolateFn) {
        directives.push({
          priority: 0,
          compile: valueFn(function textInterpolateLinkFn(scope, node) {
            var parent = node.parent(),
                bindings = parent.data('$binding') || [];
            bindings.push(interpolateFn);
            safeAddClass(parent.data('$binding', bindings), 'ng-binding');
            scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
              node[0].nodeValue = value;
            });
          })
        });
      }
    }

像這樣的文字節點就會自動構建上面的指令,自動新增一個監聽,通過修改原生方法來修改節點的值

<body>
    {{ feenan }}
</body>
  • comment 8 註釋的節點也能自動的新增指令

上面的三種情況的新增指令方法是addDirective

function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName,endAttrName) {
      if (name === ignoreDirective) return null;
      var match = null;
      if (hasDirectives.hasOwnProperty(name)) {
        for(var directive, directives = $injector.get(name + Suffix),
            i = 0, ii = directives.length; i<ii; i++) {
          try {
            directive = directives[i];
            if ( (maxPriority === undefined || maxPriority > directive.priority) &&
                 directive.restrict.indexOf(location) != -1) {
              if (startAttrName) {
                directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
              }
              tDirectives.push(directive);
              match = directive;
            }
          } catch(e) { $exceptionHandler(e); }
        }
      }
      return match;
    }

這裡就會用到這個物件hasDirectives,這就是系統在初始化的時候新增的一個內健指令物件集合.
假如節點的名稱在這個物件,則把指令新增到傳遞進來的tDirectives陣列內.返回當前指令.

蒐集完指令之後,就要開始使用了,接下來呼叫applyDirectivesToNode方法,這個方法將會生成最終連結時候呼叫的link函式

applyDirectivesToNode會對directives進行loop,依次檢查指令的屬性,這裡以compile屬性來說,當檢測到指令有compile屬性,則

if (directive.compile) {
              try {
                linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
                if (isFunction(linkFn)) {
                  addLinkFns(null, linkFn, attrStart, attrEnd);
                } else if (linkFn) {
                  addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
                }
              } catch (e) {
                $exceptionHandler(e, startingTag($compileNode));
              }
            }

執行directive.compile方法,返回一個linkFn,然後呼叫addLinkFns新增到內部陣列中,這裡是postLinkFns陣列,最終執行使用者定義的linkFn或者系統自帶的,都會訪問這個陣列的內容

最後applyDirectivesToNode返回的是一個內部函式nodeLinkFn,這個就是呼叫使用者定義指令函式的發起者.

當前節點指令處理完之後,然後開始查詢子節點的指令,基本上跟父節點規則一樣,最後返回compositeLinkFn函式給compositeLinkFn內部變數,這個下面會用到,最後整個compile函式返回publicLinkFn函式

到這裡compile(element)就執行完了,再來說說第二步,最終進行指令連結

publicLinkFn(scope)

首先scope是根作用域,這個方法主要是執行所有的連結函式,新增監聽函式.

function publicLinkFn(scope, cloneConnectFn, transcludeControllers){
    assertArg(scope, 'scope');
    var $linkNode = cloneConnectFn
      ? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!!
      : $compileNodes;

    forEach(transcludeControllers, function(instance, name) {
      $linkNode.data('$' + name + 'Controller', instance);
    });

    // 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  || nodeType === 9 ) {
        $linkNode.eq(i).data('$scope', scope);
      }
    }

    if (cloneConnectFn) cloneConnectFn($linkNode, scope);
    if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode);
    return $linkNode;
};

把當前作用域儲存到元素的data裡,然後呼叫第一步裡的compositeLinkFn函式,傳遞根作用域和根節點

這個會呼叫compileNodes裡的compositeLinkFn方法,此時閉包屬性linkFns屬性裡儲存了兩個項nodeLinkFn, childLinkFn,根節點的nodelinkFn為空,childlinkFn有值,它的值本身也是一個compositeLinkFn函式,然後傳遞根節點的子節點進去,最終當nodelinkFn有值的時候,會呼叫applyDirectivesToNode內部的nodeLinkFn方法,上面說了,這個呼叫所有連結函式的發起者.

nodeLinkFn程式碼比較長,就不貼了,這裡主要做了以下幾件事

  • 根據指令的scope屬性來構建作用域資訊

  • 是否需要構建控制器,此時會呼叫控制器的初始化資訊

  • 執行prelinkfns,postlinkfns陣列內的連結函式,這些都是在第一步收集好的

核心程式碼如下

// 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.require, $element, elementControllers), transcludeFn);
  } catch (e) {
    $exceptionHandler(e, startingTag($element));
  }
}

// RECURSION
// We only pass the isolate scope, if the isolate directive has a template,
// otherwise the child elements do not belong to the isolate directive.
var scopeToChild = scope;
if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
  scopeToChild = isolateScope;
}
childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);

// POSTLINKING
for(i = postLinkFns.length - 1; i >= 0; i--) {
  try {
    linkFn = postLinkFns[i];
    linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
        linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn);
  } catch (e) {
    $exceptionHandler(e, startingTag($element));
  }
}

執行流程為preLinkFns -> childLinkFn -> postLinkFns

最終執行連結的函式在這裡

linkFn(linkFn.isolateScope ? isolateScope : scope, $element, attrs,
        linkFn.require && getControllers(linkFn.require, $element, elementControllers), transcludeFn);

這裡就會執行使用者自定義的指令內容,以及系統自帶的指令內容,像上面文字節點對應的指令內容,像下面的這個

function textInterpolateLinkFn(scope, node) {
    var parent = node.parent(),
        bindings = parent.data('$binding') || [];
    bindings.push(interpolateFn);
    safeAddClass(parent.data('$binding', bindings), 'ng-binding');
    scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
      node[0].nodeValue = value;
    });
}

指令內容裡可以新增監聽,寫一些DOM操作的程式碼,都是可以的

總結

以上只是對編譯服務的一些簡單理解,有啥錯誤的希望大家指出來,一起進步,以後有空再分析下業務相關的Provider.

歡迎轉載,轉載請註明作者和出處:feenan