Angular中$compile源碼分析
$compile,在Angular中即“編譯”服務,它涉及到Angular應用的“編譯”和“鏈接”兩個階段,根據(jù)從DOM樹遍歷Angular的根節(jié)點(ng-app)和已構(gòu)造完畢的 \$rootScope對象,依次解析根節(jié)點后代,根據(jù)多種條件查找指令,并完成每個指令相關(guān)的操作(如指令的作用域,控制器綁定以及transclude等),最終返回每個指令的鏈接函數(shù),并將所有指令的鏈接函數(shù)合成為一個處理后的鏈接函數(shù),返回給Angluar的bootstrap模塊,最終啟動整個應用程序。
[TOC]
Angular的compileProvider
拋開Angular的MVVM實現(xiàn)方式不談,Angular給前端帶來了一個軟件工程的理念-依賴注入DI。依賴注入從來只是后端領(lǐng)域的實現(xiàn)機制,尤其是javaEE的spring框架。采用依賴注入的好處就是無需開發(fā)者手動創(chuàng)建一個對象,這減少了開發(fā)者相關(guān)的維護操作,讓開發(fā)者無需關(guān)注業(yè)務邏輯相關(guān)的對象操作。那么在前端領(lǐng)域呢,采用依賴注入有什么與之前的開發(fā)不一樣的體驗呢?
我認為,前端領(lǐng)域的依賴注入,則大大減少了命名空間的使用,如著名的YUI框架的命名空間引用方式,在極端情況下對象的引用可能會非常長。而采用注入的方式,則消耗的僅僅是一個局部變量,好處自然可見。而且開發(fā)者僅僅需要相關(guān)的“服務”對象的名稱,而不需要知道該服務的具體引用方式,這樣開發(fā)者就完全集中在了對象的借口引用上,專注于業(yè)務邏輯的開發(fā),避免了反復的查找相關(guān)的文檔。
前面廢話一大堆,主要還是為后面的介紹做鋪墊。在Angular中,依賴注入對象的方式依賴與該對象的Provider,正如小結(jié)標題的compileProvider一樣,該對象提供了compile服務,可通過injector.invoke(compileProvider.$get,compileProvider)函數(shù)完成compile服務的獲取。因此,問題轉(zhuǎn)移到分析compileProvider.\$get的具體實現(xiàn)上。
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五個服務,分別用于實現(xiàn)“依賴注入的注入器(\$injector),js代碼解析器(\$parse),控制器服務(\$controller),根作用域(\$rootScope),http服務和指令解析服務”。compileProvider通過這幾個服務單例,完成了從抽象語法樹的解析到DOM樹構(gòu)建,作用域綁定并最終返回合成的鏈接函數(shù),實現(xiàn)了Angular應用的開啟。
\$get方法最終返回compile函數(shù),compile函數(shù)就是\$compile服務的具體實現(xiàn)。下面我們深入compile函數(shù):
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函數(shù),針對所需要遍歷的根節(jié)點開始,完成指令的解析,并生成合成之后的鏈接函數(shù),返回一個publicLinkFn函數(shù),該函數(shù)完成根節(jié)點與根作用域的綁定,并在根節(jié)點緩存指令的控制器實例,最終執(zhí)行合成鏈接函數(shù)。
合成鏈接函數(shù)的生成
通過上一小結(jié),可以看出\$compile服務的核心在于compileNodes函數(shù)的執(zhí)行及其返回的合成鏈接函數(shù)的執(zhí)行。下面,我們深入到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 }); } }); // 執(zhí)行指令的鏈接函數(shù) 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數(shù)組用于存儲每個DOM節(jié)點上所有指令的處理后的鏈接函數(shù)和子節(jié)點上所有指令的處理后的鏈接函數(shù),具體使用遞歸的方式實現(xiàn)。隨后,在返回的compositeLinkFn中,則是遍歷linkFns,針對每個鏈接函數(shù),創(chuàng)建起對應的作用域?qū)ο螅ㄡ槍?chuàng)建隔離作用域的指令,創(chuàng)建隔離作用域?qū)ο螅⒈4嬖诠?jié)點的緩存中),并處理指令是否設置了transclude屬性,生成相關(guān)的transclude處理函數(shù),最終執(zhí)行鏈接函數(shù);如果當前指令并沒有鏈接函數(shù),則調(diào)用其子元素的鏈接函數(shù),完成當前元素的處理。
在具體的實現(xiàn)中,通過collectDirectives函數(shù)完成所有節(jié)點的指令掃描。它會根據(jù)節(jié)點的類型(元素節(jié)點,注釋節(jié)點和文本節(jié)點)分別按特定規(guī)則處理,對于元素節(jié)點,默認存儲當前元素的標簽名為一個指令,同時掃描元素的屬性和CSS class名,判斷是否滿足指令定義。
緊接著,執(zhí)行applyDirectivesToNode函數(shù),執(zhí)行指令相關(guān)操作,并返回處理后的鏈接函數(shù)。由此可見,applyDirectivesToNode則是\$compile服務的核心,重中之重!
applyDirectivesToNode函數(shù)
applyDirectivesToNode函數(shù)過于復雜,因此只通過簡單代碼說明問題。
上文也提到,在該函數(shù)中執(zhí)行用戶定義指令的相關(guān)操作。
首先則是初始化相關(guān)屬性,通過遍歷節(jié)點的所有指令,針對每個指令,依次判斷$$start屬性,優(yōu)先級,隔離作用域,控制器,transclude屬性判斷并編譯其模板,構(gòu)建元素的DOM結(jié)構(gòu),最終執(zhí)行用戶定義的compile函數(shù),將生成的鏈接函數(shù)添加到preLinkFns和postLinkFns數(shù)組中,最終根據(jù)指令的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替換當前元素節(jié)點,再重新編譯原先的DOM節(jié)點,而如果transclude設置為默認的true時,則會繼續(xù)編譯其子節(jié)點,并通過transcludeFn傳遞編譯后的DOM對象,完成用戶自定義的DOM處理。
在返回的nodeLinkFn中,根據(jù)用戶指令的定義,如果指令帶有隔離作用域,則創(chuàng)建一個隔離作用域,并在當前的dom節(jié)點上綁定ng-isolate-scope類名,同時將隔離作用域緩存到dom節(jié)點上;
接下來,如果dom節(jié)點上某個指令定義了控制器,則會調(diào)用\$cotroller服務,通過依賴注入的方式(\$injector.invoke)獲取該控制器的實例,并緩存該控制器實例;
隨后,調(diào)用initializeDirectiveBindings,完成隔離作用域?qū)傩缘膯蜗蚪壎ǎˊ),雙向綁定(=)和函數(shù)的引用(&),針對隔離作用域的雙向綁定模式(=)的實現(xiàn),則是通過自定義的編譯器完成簡單Angular語法的編譯,在指定作用域下獲取表達式(標示符)的值,保存為lastValue,并通過設置parentValueFunction添加到當前作用域的$watch數(shù)組中,每次\$digest循環(huán),判斷雙向綁定的屬性是否變臟(dirty),完成值的同步。
最后,根據(jù)applyDirectivesToNode第一步的初始化操作,將遍歷執(zhí)行指令compile函數(shù)返回的鏈接函數(shù)構(gòu)造出成的preLinkFns和postLinkFns數(shù)組,依次執(zhí)行,如下所示:
_.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 ); });
可以看出,首先執(zhí)行preLinkFns的函數(shù);緊接著遍歷子節(jié)點的鏈接函數(shù),并執(zhí)行;最后執(zhí)行postLinkFns的函數(shù),完成當前dom元素的鏈接函數(shù)的執(zhí)行。指令的compile函數(shù)默認返回postLink函數(shù),可以通過compile函數(shù)返回一個包含preLink和postLink函數(shù)的對象設置preLinkFns和postLinkFns數(shù)組,如在preLink針對子元素進行DOM操作,效率會遠遠高于在postLink中執(zhí)行,原因在于preLink函數(shù)執(zhí)行時并未構(gòu)建子元素的DOM,在當子元素是個擁有多個項的li時尤為明顯。
end of compile-publicLinkFn
終于,到了快結(jié)束的階段了。通過compileNodes返回從根節(jié)點(ng-app所在節(jié)點)開始的所有指令的最終合成鏈接函數(shù),最終在publicLinkFn函數(shù)中執(zhí)行。在publicLinkFn中,完成根節(jié)點與根作用域的綁定,并在根節(jié)點緩存指令的控制器實例,最終執(zhí)行合成鏈接函數(shù),完成了Angular最重要的編譯,鏈接兩個階段,從而開始了真正意義上的雙向綁定。
相關(guān)文章
angularjs1.5 組件內(nèi)用函數(shù)向外傳值的實例
今天小編就為大家分享一篇angularjs1.5 組件內(nèi)用函數(shù)向外傳值的實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09AnjularJS中$scope和$rootScope的區(qū)別小結(jié)
這篇文章給大家整理了關(guān)于AnjularJS中$scope和$rootScope的區(qū)別,文中運用實例代碼介紹的很詳細,有需要的朋友們可以參考借鑒,下面來一起看看吧。2016-09-09angular.js + require.js構(gòu)建模塊化單頁面應用的方法步驟
這篇文章主要給大家介紹了關(guān)于利用angular.js + require.js構(gòu)建模塊化單頁面應用的方法步驟,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起看看吧。2017-07-07angularjs在ng-repeat中使用ng-model遇到的問題
本文給大家分享了一個個人在使用angular過程中遇到的在ng-repeat中使用ng-model的問題,并附上簡單的解決辦法,希望能對大家學習angular有所幫助2016-01-01