OK!昨天介紹了一個強而有力的 tag:ng-content,今天不免俗的就要來爬一下 source code,了解在 content projection 的機制背後,Angular 做了那些不為人知的事情。
<div style="border: 2px solid blue; padding: 1rem;">
<p>project works!</p>
<p>This is the view of PrjectComponent</p>
<ng-content></ng-content>
</div>
↑ Block 1
function ProjectComponent_Template(rf, ctx) {
if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojectionDef"]();
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "div", 0);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](1, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](2, "project works!");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](3, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](
4,
"This is the view of PrjectComponent"
);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](5);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
}
}
↑ Block 2
Block 2 是 Block 1 被編譯之後的部分結果,我想前幾天我們已經看過夠多被編譯過的程式碼,我這次只需要把重要的部分貼上來就好。
ProjectComponent_Template 這個函式是 Angular 在 runtime 時會拿來建立 DOM 元素的依據,其中由 ɵɵ
開頭的函式,都是被稱作 instruction
的操作,今天的重點有兩個,一個是最開頭的 ɵɵprojectionDef
instruction,第二個是倒數第二個被呼叫的 instruction:ɵɵprjection
。
export function ɵɵprojectionDef(projectionSlots?: ProjectionSlots): void {
const componentNode = getLView()[DECLARATION_COMPONENT_VIEW][T_HOST] as TElementNode;
if (!componentNode.projection) {
// If no explicit projection slots are defined, fall back to a single
// projection slot with the wildcard selector.
const numProjectionSlots = projectionSlots ? projectionSlots.length : 1;
const projectionHeads: (TNode|null)[] = componentNode.projection =
newArray(numProjectionSlots, null! as TNode);
const tails: (TNode|null)[] = projectionHeads.slice();
// 下接 Block 4
}
}
↑ Block 3:ɵɵprojectionDef
在進到 ɵɵprojectionDef 函式之後,Angular 會先依照 projection
屬性來判斷當前的 TNode 有沒有被處理過,有的話就跳過!
如果 projection 這個屬性沒有東西的話就會依照 template 上有的 projectionSlots 數量來初始化一個長度一樣的陣列。如果 projectionSlots 沒有東西,就會給一個 1
,使用 *
作為 selector,也就是預設將所有元素投影到這個 slot。
let componentChild: TNode|null = componentNode.child;
while (componentChild !== null) {
const slotIndex =
projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0;
if (slotIndex !== null) {
if (tails[slotIndex]) {
tails[slotIndex]!.projectionNext = componentChild;
} else {
projectionHeads[slotIndex] = componentChild;
}
tails[slotIndex] = componentChild;
}
componentChild = componentChild.next;
}
↑ Block 4
一旦完成 Block 3 的前置作業,就可以開始來做配對囉!
Block 4 在進入 while 迴圈的時候,會先呼叫一個 matchingProjectionSlotIndex 方法,這個方法就如同它的名稱一樣,就是拿來比對給定的 componentChild 身上的 attributes 有沒有符合 projectionSlots 所要求的,若有符合,就回傳該 slot 的 index,藉以辨識要將 componentChild 投影到哪一個 slot。
以今天最開始的 Block 1 為範例的話,我們會直接得到 0
因為根本沒有傳 projectionSlots 進來。
接著就會來設定 projection 這個最一開始被設定的屬性了!
最後就是透過 ɵɵprojection 這個 instruction 來把剛剛排序好的 TNode 們放進 DOM 囉:
export function ɵɵprojection(
nodeIndex: number, selectorIndex: number = 0, attrs?: TAttributes): void {
const lView = getLView();
const tView = getTView();
const tProjectionNode =
getOrCreateTNode(tView, nodeIndex, TNodeType.Projection, null, attrs || null);
// We can't use viewData[HOST_NODE] because projection nodes can be nested in embedded views.
if (tProjectionNode.projection === null) tProjectionNode.projection = selectorIndex;
// `<ng-content>` has no content
setCurrentTNodeAsNotParent();
// We might need to delay the projection of nodes if they are in the middle of an i18n block
if (!delayProjection) {
// re-distribution of projectable nodes is stored on a component's view level
applyProjection(tView, lView, tProjectionNode);
}
}
以上就是單一個 ng-content 時的投影流程,因為只有單一個 ng-content 所以看不太出來 tails 與 projectionHeads 的優美設計,也沒有特別說明 Angular 比對 attribute 的作法,我們明天用另一個更複雜的範例再來詳細說明!
以下按照入團順序列出我們團隊夥伴的系列文章!
請自由參閱 ?