@(Angular)
$compile,在Angular中即“編譯”服務,它涉及到Angular應用的“編譯”和“鏈接”兩個階段,根據從DOM樹遍歷Angular的根節點(ng-app)和已構造完畢的 $rootScope對象,依次解析根節點后代,根據多種條件查找指令,並完成每個指令相關的操作(如指令的作用域,控制器綁定以及transclude等),最終返回每個指令的鏈接函數,並將所有指令的鏈接函數合成為一個處理后的鏈接函數,返回給Angluar的bootstrap模塊,最終啟動整個應用程序。
Angular的compileProvider
拋開Angular的MVVM實現方式不談,Angular給前端帶來了一個軟件工程的理念-依賴注入DI。依賴注入從來只是后端領域的實現機制,尤其是javaEE的spring框架。采用依賴注入的好處就是無需開發者手動創建一個對象,這減少了開發者相關的維護操作,讓開發者無需關注業務邏輯相關的對象操作。那么在前端領域呢,采用依賴注入有什么與之前的開發不一樣的體驗呢?
我認為,前端領域的依賴注入,則大大減少了命名空間的使用,如著名的YUI框架的命名空間引用方式,在極端情況下對象的引用可能會非常長。而采用注入的方式,則消耗的僅僅是一個局部變量,好處自然可見。而且開發者僅僅需要相關的“服務”對象的名稱,而不需要知道該服務的具體引用方式,這樣開發者就完全集中在了對象的借口引用上,專注於業務邏輯的開發,避免了反復的查找相關的文檔。
前面廢話一大堆,主要還是為后面的介紹做鋪墊。在Angular中,依賴注入對象的方式依賴與該對象的Provider,正如小結標題的compileProvider一樣,該對象提供了compile服務,可通過injector.invoke(compileProvider.$get,compileProvider)函數完成compile服務的獲取。因此,問題轉移到分析compileProvider.$get的具體實現上。
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的具體邏輯中去:
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最重要的編譯,鏈接兩個階段,從而開始了真正意義上的雙向綁定。