Angular中$compile源碼分析,angularcompile
$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.\$getthis.\$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最重要的編譯,連結兩個階段,從而開始了真正意義上的雙向繫結。
您可能感興趣的文章:
- angularjs指令中的compile與link函數詳解
- AngularJS初始化過程分析(引導程式)
- AngularJS學習筆記之TodoMVC的分析
- 對比分析AngularJS中的$http.post與jQuery.post的區別