compile.jsメモ

AngularJSのcompile.jsを読んだ際のメモ

priorityは高いものから順番に実行されれる。
terminalがtrueだとそのpriorityで終わり。

$CompileProvider($provide, $$sanitizeUriProvider)

var hasDirectives = {},
    Suffix = 'Directive',
    COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w_\-]+)\s+(.*)$/,
    CLASS_DIRECTIVE_REGEXP = /(([\d\w_\-]+)(?:\:([^;]+))?;?)/,
    ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset');

function registerDirective(name, directiveFactory)

// directiveFactoryの例
var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sniffer, $filter) {
  return {
    restrict: 'E',
    require: ['?ngModel'],
    link: function(scope, element, attr, ctrls) {
      if (ctrls[0]) {
        (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
                                                            $browser, $filter);
      }
    }
  };
}];

$compileProvider.directive(name, directiveFactory)の実体である。
nameはcamel case化したものである。
providerCacheにdirectiveを生成する関数をname + “Directive”というキー名で登録する。
hasDirectives[name]が存在していない場合、directiveFactoryをhasDirectives[name]に登録する。


$CompileProvider::$get()

function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext)を返す。

var startSymbol = $interpolate.startSymbol(),
    endSymbol = $interpolate.endSymbol(),
    denormalizeTemplate = (startSymbol == ' || endSymbol  == ')
        ? identity
        : function denormalizeTemplate(template) {
          return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
    },
    NG_ATTR_BINDING = /^ngAttr[A-Z]/;


Attributes(element, attr)


Attributes::$normalize(name)

nameをcamel caseに変換する。

Attributes::$addClass(classVal)

コンストラクタの引数のelementのclass属性にclassValを追加する。

Attributes::$removeClass(classVal)

コンストラクタの引数のelementのclass属性にclassValを削除する。

Attributes::$updateClass(newClasses, oldClasses)

コンストラクタの引数のelementのclass属性にnewClassesとoldClassesの差分を反映する。

Attributes::$set(key, value, writeAttr, attrName)

key: directive(\(element)にある属性型のdirectiveのcamel case化した名前や directiveのscopeに存在しているモデル名 (ex: ngIf) value: keyの値 (ex: "foo_id != 0") writeAttr: directive(\)element)にattrName=valueを属性として書き込むか (デフォルトはtrue)
attrName: directive($$element)に書き込まれる属性名 (デフォルトはkeyをsnake caseに変換したもの)

// していることの概要
// Attributesインスタンスのkey属性にvalueを保存する
this[key] = value;    
// $attrにkeyをキーとしてsnake caseにして保存する
this.$attr[key] = attrName = snake_case(key, '-');
// elementの属性に値を書き込む
this.$$element.attr(attrName, value);
// $$observersに登録されている関数を実行する。
observer = key
var $$observers = this.$$observers;
$$observers && forEach($$observers[observer], function(fn) {
    try {
        fn(value);
    } catch (e) {
        $exceptionHandler(e);
    }
});

Attributes::$observe(key, fn)

key: directive(\(element)にある属性型のdirectiveのcamel case化した名前や directiveのscopeに存在しているモデル名 (ex: ngIf) fn:\)observersに格納される関数です。keyの値が変更されるたびに実行される。
\(observersにfnを格納する。\)observersからfnを削除する関数を返す。

$rootScope.$evalAsync(function() {
    if (!listeners.$$inter) {
        // no one registered attribute interpolation function, so lets call it manually
        fn(attrs[key]);
    }
});


compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext)

$compile.$get()の戻り値
$compileNodesをjqLiteオブジェクトにする。
$compileNodesの要素の中で空文字でないTextNodeはでwrappする。

// publicLinkFn内で利用する
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes,
                                       maxPriority, ignoreDirective, previousCompileContext);

$compileNodesのclass属性にng-scopeを追加する。
これを返すfunction publicLinkFn(scope, cloneConnectFn, transcludeControllers, parentBoundTranscludeFn)

compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext)

compileで利用されるcompositeLinkFnを返す。
nodeListのnodeごとにAttributesインスタンスを生成する。
nodeListのnodeごとにcollectDirectives()を実行する。
nodeListのnodeごとにaddDirectivesToNode()を実行する。
nodeLinkFn.scopeがtrue(directive.scope === true)なら対象の要素のclassに’ng-scope’を加える。

collectDirectives(node, directives, attrs, maxPriority, ignoreDirective)

compileNodes()で利用されている。
nodeに存在しているdirectiveを見つける。
そのdirectiveのデータを生成する。
directiveのデータをdirectivesに格納する。
directivesを返す。

nodeのnodeTypeがElement(1)の場合
以下をチェックして該当した場合、 directivesに加える。

要素の属性データを登録する。

// attrsMapはattrs.$attrのこと      
// nName: "ngRepeat"  
// name: "ng-repeat"  
// value: "e in entries"
attrsMap[nName] = name;
attrs[nName] = value = trim(attr.value);

nodeのnodeTypeがText Node(3)の場合
addTextInterpolateDirective(directives, text)を実行する

nodeのnodeTypeがComment(8)の場合
COMMENT_DIRECTIVE_REGEXPにマッチした場合、addDirective()を実行する
byPriorityでpriorityを降順にソートする。

//directivesの値の例
[
    {
        compile: function () {return value;},
        index: 0,
        link: function (scope, $element, attr, ctrl, $transclude) { },
        name: "ngView",
        priority: 400,
        require: undefined,
        restrict: "ECA",
        terminal: true,
        transclude: "element"
    },
    {
        compile: function () {return value;}
        index: 1
        link: function (scope, $element) {}
        name: "ngView"
        priority: -400
        require: undefined
        restrict: "ECA"
    },
]


addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName)

collectDirectives()内で呼ばれる。
tDirectivesにdirectiveのデータを格納する。
hasDirectives($compileProvider.directiveで登録)にnameキーが存在した場合、
directives = $injector.get(name + Suffix)
directivesの各データごとにdirectiveがAでstart属性とend属性が設定されていた場合、それらをdirective.\(startとdirective.\)endに付与する。
tDirectivesにdirectiveのデータを格納する。

applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext)

jqCollectionはroot of compile tree
nodeLinkFnを返す。
compileNodes()内で利用しています。

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

要素に存在しているdirectiveごとに以下の処理をする。
directiveValueには一時的に使用する値を代入する。


compileTemplateUrl(directives, $compileNode, tAttrs, $rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext)

applyDirectivesToNode()内で利用されている
nodeLinkFn上書きするdelayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn)を生成して返す。

nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
  templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
    controllerDirectives: controllerDirectives,
    newIsolateScopeDirective: newIsolateScopeDirective,
    templateDirective: templateDirective,
    nonTlbTranscludeDirective: nonTlbTranscludeDirective
  });

$compileNodeの子要素を削除する。
$http.get(templateUrl)でテンプレートを取得する。
現在処理中のdirectiveに対してreplaceが設定されている場合、それに対応した処理をする。
directive.templateとと処理内容はほぼ同じだが、applyDirectivesToNode(), compileNode()を実行する。
delayedNodeLinkFnと$http.get()のコールバック関数のどちらが先に実行されるかで処理の内容が異なる。
それは、linkQueueに要素数があるかどうかで決まる。

delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn)

templateUrlが指定されていた場合のnodeLinkFnの実体
applyDirectivesToNode()の戻り値