好的,昨天用了一個簡單的範例來說明 ɵɵprojectionDef
以及 ɵɵprojection
這兩個 instructions,今天來用 multi ng-content 的範例來看一下 Angular 是怎麼進行 slot 分配的吧。
<app-mproject>
<div footer>
<p>FOOTER</p>
</div>
<div class="header">
<p>HEADER</p>
</div>
<p>Not a footer is a p element</p>
<span>Third</span>
<p class="header" footer>Second</p>
</app-mproject>
↑ Block 1
const _c0 = [[["", 8, "header"]], [["", "footer", ""]], [["p"]], "*"];
const _c1 = [".header", "[footer]", "p", "*"];
↑ Block 2
MprojectComponent.ɵcmp = _angular_core__WEBPACK_IMPORTED_MODULE_0__[
"ɵɵdefineComponent"
]({
type: MprojectComponent,
selectors: [["app-mproject"]],
ngContentSelectors: _c1,
// ... 略
template: function MprojectComponent_Template(rf, ctx) {
if (rf & 1) {
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojectionDef"](_c0);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "div", 0);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](1);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](2, "div", 1);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](3, "p");
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](
4,
"mproject works!"
);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](5, 1);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](6, 2);
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵprojection"](7, 3);
}
},
// ... 略
});
↑ Block 3
Block 2 的程式碼定義了兩個 const 變數,一個是 _c0,是之後要傳進 ɵɵprojectionDef 的 projectionSlots
參數,另一個 _c1 則是被放在 ngContentSelectors
屬性內。
昨天我們看了 ɵɵprojectionDef
這個 instruction,今天要更仔細地往底層看,看一下同個頁面有多個 ng-content 的時候,Angular 會怎麼進行處理。
const projectionHeads: (TNode|null)[] = componentNode.projection =
newArray(numProjectionSlots, null! as TNode);
const tails: (TNode|null)[] = projectionHeads.slice();
↑ Block 3
在 ɵɵprojectionDef 中這兩個 array 佔有很大的地位,首先 projectionHeads 與 componentNode.projection 是同一個陣列,各位應該都沒有問題,每當我們針對 projectionHeads 操作時,componentNode.projection 也會跟著有反應。
但 tails 的存在就有點特別了,因為他是由 projectionHeads 透過 slice 方法產生的另一個新的陣列,乍看下所有對 tail 的操作都不會反應給外不知道,那麼這個陣列是拿來做什麼的呢?這部分待會就會說明。
當進到 while 迴圈後,因為傳入的 projectionSlots 不再是 undefined,所以 Angular 會呼叫 matchingProjectionSlotIndex 這個函式,來協助取得該元素可以被投影的 slot 的 index:
const slotIndex =
projectionSlots ? matchingProjectionSlotIndex(componentChild, projectionSlots) : 0;
↑ Block 4
export function matchingProjectionSlotIndex(tNode: TNode, projectionSlots: ProjectionSlots): number|
null {
let wildcardNgContentIndex = null;
const ngProjectAsAttrVal = getProjectAsAttrValue(tNode);
// ... 下接 Block 7
}
↑ Block 5
進到 matchingProjectionSlotIndex 之後,Angular 會先透過 getProjectAsAttrValue 這個函式,來尋找傳進來的 tNode 身上有沒有 projectAs
這個 attribute,
projectAs 這個 attribute 不在今天文章的討論範圍,所以這個函式就也先不講,同時因為我們的範例程式中也沒有使用到這個 attribute,所以 getProjectAsAttrValue 函式會回傳一個 null。
接著就開始針對 projectionSlots 內的 slot 與 tNode(componentChild)做比對啦。
先回憶一下傳入的 projectionSlots,也就是最開頭 Block 2 提到的 _c0 變數:
const _c0 = [
[["", 8, "header"]],
[["", "footer", ""]],
[["p"]],
"*"
];
↑ Block 6
projectionSlots 的型別是一個 CssSelectorList 陣列或是 *
,它會長得這麼特殊是因為已經被 Angular compiler 處理過了,有興趣的可以到 packages/core/src/render3/interfaces/projection.ts 這個檔案去一探究竟喔!
讓我們回到比對的部分:
for (let i = 0; i < projectionSlots.length; i++) {
const slotValue = projectionSlots[i];
// The last wildcard projection slot should match all nodes which aren't matching
// any selector. This is necessary to be backwards compatible with view engine.
if (slotValue === '*') {
wildcardNgContentIndex = i;
continue;
}
// If we ran into an `ngProjectAs` attribute, we should match its parsed selector
// to the list of selectors, otherwise we fall back to matching against the node.
if (ngProjectAsAttrVal === null ?
isNodeMatchingSelectorList(tNode, slotValue, /* isProjectionMode */ true) :
isSelectorInSelectorList(ngProjectAsAttrVal, slotValue)) {
return i; // first matching selector "captures" a given node
}
}
return wildcardNgContentIndex;
↑ Block 7
進到比對 projectionSlots 的階段後,若馬上就遇到一個是 *
的預設 ng-content 的話,Angular 會先記錄這個 * slot 的 index,對於 Angular 的設計來說,這個 * slot 應該要是最後才選擇的。所以紀錄之後,會再往下遍歷剩下的 slot,直到沒有其他更合適的 slot 才會回傳 * slot 的 index。
來看一下 isNodeMatchingSelectorList 函式,它會透過呼叫 isNodeMatchingSelector
來檢查傳入的 tNode 是否有符合傳入 selector,然後回傳一個 boolean:
export function isNodeMatchingSelectorList(
tNode: TNode, selector: CssSelectorList, isProjectionMode: boolean = false): boolean {
for (let i = 0; i < selector.length; i++) {
if (isNodeMatchingSelector(tNode, selector[i], isProjectionMode)) {
return true;
}
}
return false;
↑ Block 8
如果有符合的話,Angular 就會直接回傳當前 slot 的 index。
然後就可以進入下一個階段,將 componentChild 放入 tails 與 projectionHeads:
if (slotIndex !== null) {
if (tails[slotIndex]) {
tails[slotIndex]!.projectionNext = componentChild;
} else {
projectionHeads[slotIndex] = componentChild;
}
tails[slotIndex] = componentChild;
}
componentChild = componentChild.next;
↑ Block 9
首先當 tails[slotIndex]
內沒有存 componentChild 的話,Angular 會先將這個 componentChild 存入 projectionHeads[soltIndex]
,然後也會再將相同的 componentChild 存入 tails[soltIndex]
。
下次當有另一個 componentChild' 取得相同的 slotIndex 時,Angular 會將這個 componentChild' 存入 tails[slotIndex] 的 projectionNext 屬性,最後在存入 tails[slotIndex] 將它取代。
有看到這個設計的絕妙之處嗎?
Angular 團隊透過操作 projectionHeads 與 tails 兩個陣列,一方面可以用 projecitonHeads 來保留每一個 slot 最初遇到的 component,一方面透過 tails 來設定前一個 component 的 projectionNext 屬性、並持續更新 tails 為最後一個要放入 slot 的 component,就像是單向的 linked-list 一樣!
之後的故事我想大家都知道了!
那麼今天就這樣啦,祝各位另一個連假快樂!
以下按照入團順序列出我們團隊夥伴的系列文章!
請自由參閱 ?