原文鏈接:
Here is why you will not find components inside Angular
Component is just a directive with a template? Or is it?
從我最先運用 Angular 最先,就被組件和指令間區分的題目所疑心,特別對那些從 Angular.js 天下來的人,因為 Angular.js 里只要指令,只管我們也常常把它當作組件來運用。假如你在網上搜這個題目詮釋,許多都邑這麼詮釋(注:為清楚邃曉,不翻譯):
Components are just directives with a content defined in a template…
Angular components are a subset of directives. Unlike directives, components always have…
Components are high-order directives with templates and serve as…
這些說法貌似都對,我在檢察由 Angular 編譯器編譯組件天生的視圖工場源碼里,確實沒發明組件定義,你假如檢察也只會發明 指令。
注:運用 Angular-CLI ng new 一個新項目,實行 ng serve 運轉順序后,就可在 Chrome Dev Tools 的 Source Tab 的 ng:// 域下檢察到編譯組件後天生的 **.ngfactory.js 文件,該文件代碼即上面說的視圖工場源碼。
然則我在網上沒有找到 緣由詮釋,因為想要曉得緣由就必須對 Angular 內部事變道理比較熟習,假如上面的題目也困讓了你很長一段時間,那本文正合適你。讓我們一同探究个中的奧妙並做好預備吧。
本質上,本文主要詮釋 Angular 內部是怎樣定義組件和指令的,並引入新的視圖節點定義——指令定義。
注:視圖節點還包括元素節點和文本節點,有興緻可檢察
譯 Angular DOM 更新機制 。
視圖
假如你讀過我之前寫的文章,特別是 譯 Angular DOM 更新機制,你能夠會邃曉 Angular 順序內部是一棵視圖樹,每一個視圖都是由視圖工場天生的,而且每一個視圖包括具有特定功用的差別視圖節點。在方才提到的文章中(那篇文章對相識本文很主要嗷),我引見過兩個最簡樸的節點範例——元素節點定義和文本節點定義。元素節點定義是用來建立一切 DOM 元素節點,而文本節點定義是用來建立一切 DOM 文本節點 。
所以假如你寫了以下的一個模板:
<div><h1>Hello {{name}}</h1></div>
Angular Compiler 將會編譯這個模板,並天生兩個元素節點,即 div
和 h1
DOM 元素,和一個文本節點,即 Hello {{name}}
DOM 文本。這些都是很主要的節點,因為沒有它們,你在屏幕上看不到任何東西。然則組件合成形式通知我們能夠嵌套組件,所以必定另一種視圖節點來嵌入組件。為了搞清楚這些特別節點是什麼,起首須要相識組件是由什麼構成的。本質上,組件本質上是具有特定行動的 DOM 元素,而這些行動是在組件類里完成的。起首看下 DOM 元素吧。
自定義 DOM 元素
你能夠曉得在 html 里能夠建立一個新的 HTML 標籤,比方,假如不運用框架,你能夠直接在 html 里插進去一個新的標籤:
<a-comp></a-comp>
然後查詢這個 DOM 節點並搜檢範例,你會發明它是個完整正當的 DOM 元素(注:你能夠在一個 html 文件里嘗嘗這部份代碼,以至能夠寫上 <a-comp>A Component</a-comp>
,結果是能夠運轉的,緣由見下文):
const element = document.querySelector('a-comp');
element.nodeType === Node.ELEMENT_NODE; // true
瀏覽器會運用 HTMLUnknownElement 接口來建立 a-comp
元素,這個接口又繼續 HTMLElement 接口,然則它不須要完成任何屬性或要領。你能夠運用 CSS 來裝潢它,也能夠給它增加事宜監聽器來監聽一些廣泛事宜,比方 click
事宜。所以正如我說的,a-comp
是一個完整正當的 DOM 元素。
然後,你能夠把它轉變成 自定義 DOM 元素 來加強這個元素,你須要為它零丁建立一個類並運用 JS API 來註冊這個類:
class AComponent extends HTMLElement {...}
window.customElements.define('a-comp', AComponent);
這是不是和你一直在做的事變有些相似呀。
沒錯,這和你在 Angular 中定義一個組件異常相似,實際上,Angular 框架嚴厲遵照 Web 組件規範然則為我們簡化了許多事變,所以我們沒必要本身建立 shadow root
並掛載到宿主元素(注:關於 shadow root
的觀點網上材料許多,實在在 Chrome Dev Tools 里,點擊右上角 settings,然後點擊 Preferences -> Elements,翻開 Show user agent shadow root
后,如許你就能夠在 Elements 面板里看到許多 DOM 元素下的 shadow root
)。但是,我們在 Angular 中建立的組件並沒有註冊為自定義元素,它會被 Angular 以特定體式格局去處置懲罰。假如你對沒有框架時怎樣建立組件很獵奇,你能夠檢察 Custom Elements v1: Reusable Web Components 。
如今已曉得,我們能夠建立任何一個 HTML 標籤並在模板里運用它。所以,假如我們在 Angular 的組件模板里運用這個標籤,框架將會給這個標籤建立元素定義(注:這是由 Angular Compiler 編譯天生的):
function View_AppComponent_0(_l) {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 1, 'a-comp', [], ...)
])
}
但是,你得須要在 module
或組件裝潢器屬性里增加 schemas: [CUSTOM_ELEMENTS_SCHEMA]
,來通知 Angular 你在運用自定義元素,不然 Angular Compiler 會拋出毛病(注:所以假如須要運用某個組件,你不得不在 module.declarations
或 module.entryComponents
或 component.entryComponents
去註冊這個組件):
'a-comp' is not a known element:
1. If 'c-comp' is an Angular component, then ...
2. If 'c-comp' is a Web Component then add...
所以,我們已有了 DOM 元素然則還沒有附着在元素上的類呢,那 Angular 里除了組件外另有其他特別類沒?當然有——指令。讓我們看看指令有些啥。
指令定義
你能夠曉得每一個指令都有一個挑選器,用來掛載到特定的 DOM 元素上。大多數指令運用屬性挑選器(attribute selectors),然則有一些也挑選元素挑選器(element selectors)。實際上,Angular 表單指令就是運用 元素挑選器 form 來把特定行動附着在 html form
元素上。
所以,讓我們建立一個空指令類,並把它附着在自定義元素上,再看看視圖定義是什麼樣的:
@Directive({selector: 'a-comp'})
export class ADirective {}
然後核對下天生的視圖工場:
function View_AppComponent_0(_l) {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 1, 'a-comp', [], ...),
jit_directiveDef4(16384, null, 0, jit_ADirective5, [],...)
], null, null);
}
如今 Angular Compiler 在視圖定義函數的第二個參數數組裡,增加了新天生的指令定義 jit_directiveDef4
節點,並放在元素定義節點 jit_elementDef3
背面。同時設置元素定義的 childCount
為 1,因為附着在元素上的一切指令都邑被看作該元素的子元素。
指令定義是個很簡樸的節點定義,它是由 directiveDef 函數天生的,該函數參數列表以下(注:如今 Angular v5.x 版本略有差別):
Name | Description |
---|---|
matchedQueries | used when querying child nodes |
childCount | specifies how many children the current element have |
ctor | reference to the component or directive constructor |
deps | an array of constructor dependencies |
props | an array of input property bindings |
outputs | an array of output property bindings |
本文我們只對 ctor 參數感興緻,它僅僅是我們定義的 ADirective
類的援用。當 Angular 建立指令對象時,它會實例化一個指令類,並存儲在視圖節點的 provider data 屬性里。
所以我們看到組件實在僅僅是一個元素定義加上一個指令定義,但僅僅如此么?你能夠曉得 Angular 老是沒那末簡樸啊!
組件展現
從上文曉得,我們能夠經由歷程建立一個自定義元素和附着在該元素上的指令,來模仿建立出一個組件。讓我們定義一個實在的組件,並把由該組件編譯天生的視圖工場類,與我們上面實驗性的視圖工場類做個比較:
@Component({
selector: 'a-comp',
template: '<span>I am A component</span>'
})
export class AComponent {}
做好預備了么?下面是天生的視圖工場類:
function View_AppComponent_0() {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 1, 'a-comp', [], ...
jit_View_AComponent_04, jit__object_Object_5),
jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)
好的,如今我們僅僅考證了上文所說的。本示例中, Angular 運用兩種視圖節點來示意組件——元素節點定義和指令節點定義。然則當運用一個實在的組件時,就會發明這兩個節點定義的參數列表照樣有些差別的。讓我們看看有哪些差別吧。
節點範例
節點範例(NodeFlags)是一切節點定義函數的第一個參數(注:最新 Angular v5.* 中參數列表有點點不一樣,如 directiveDef 中第二個參數才是 NodeFlags)。它實際上是 NodeFlags 位掩碼(注:檢察源碼,是用二進制示意的),包括一系列特定的節點信息,大部份在 變動檢測輪迴 時被框架運用。而且差別節點範例採納差別数字:16384
示意簡樸指令節點範例(注:僅僅是指令,可看 TypeDirective);49152
示意組件指令節點範例(注:組件加指令,即 TypeDirective + Component)。為了更好邃曉這些標誌位是怎樣被編譯器設置的,讓我們先轉換為二進制:
16384 = 100000000000000 // 15th bit set
49152 = 1100000000000000 // 15th and 16th bit set
假如你很獵奇這些轉換是怎樣做的,能夠檢察我寫的文章 The simple math behind decimal-binary conversion algorithms 。所以,關於簡樸指令 Angular 編譯器會設置 15-th
位為 1:
TypeDirective = 1 << 14
而關於組件節點會設置 15-th
和 16-th
位為 1:
TypeDirective = 1 << 14
Component = 1 << 15
如今邃曉為什麼這些数字差別了。關於指令來講,天生的節點被標記為 TypeDirective
節點;關於組件指令來講,天生的節點除了被標記為 TypeDirective
節點,還被標記為 Component
節點。
視圖定義剖析器
因為 a-comp
是一個組件,所以關於下面的簡樸模板:
<span>I am A component</span>
編譯器會編譯它,天生一個帶有視圖定義和視圖節點的工場函數:
function View_AComponent_0(_l) {
return jit_viewDef1(0, [
jit_elementDef2(0, null, null, 1, 'span', [], ...),
jit_textDef3(null, ['I am A component'])
Angular 是一個視圖樹,所以父視圖須要有個對子視圖的援用,子視圖會被存儲在元素節點內。本例中,a-comp
的視圖存儲在為 <a-comp></a-comp>
天生的宿主元素節點內(注:意義就是 AComponent 視圖存儲在該組件宿主元素的元素定義內,就是存在 componentView 屬性里。也能夠檢察 _Host.ngfactory.js 文件,該文件示意宿主元素 <a-comp></a-comp>
的工場,內里存儲 AComponent
視圖對象)。jit_View_AComponent_04
參數是一個 代辦類 的援用,這個代辦類將會剖析 工場函數 建立一個 視圖定義。每一個視圖定義僅僅建立一次,然後存儲在 DEFINITION_CACHE,然後這個視圖定義函數被 Angular 用來 建立視圖對象。
注:這段因為觸及大批的源碼函數,會比較艱澀。作者講的是建立視圖的詳細歷程,仔細到許多函數的挪用。總之,只須要記着一點就行:視圖剖析器經由歷程剖析視圖工場(ViewDefinitionFactory)獲得視圖(ViewDefinition)。細節暫不必管。
拿到了視圖,又該怎樣畫出來呢?看下文。
組件襯着器範例
Angular 依據組件裝潢器中定義的 ViewEncapsulation 形式來決議運用哪一種 DOM 襯着器:
以上組件襯着器是經由歷程 DomRendererFactory2 來建立的。componentRendererType
參數是在元素定義里被傳入的,本例等於 jit__object_Object_5
(注:上面代碼里有這個對象,是 jit_elementDef3()
的末了一個參數),該參數是襯着器的一個基礎描述符,用來決議運用哪個襯着器襯着組件。个中,最主要的是視圖封裝形式和所用於組件的款式(注:componentRendererType
參數的構造是 RendererType2):
{
styles:[["h1[_ngcontent-%COMP%] {color: green}"]],
encapsulation:0
}
假如你為組件定義了款式,編譯器會自動設置組件的封裝形式為 ViewEncapsulation.Emulated
,或許你能夠在組件裝潢器里顯式設置 encapsulation
屬性。假如沒有設置任何款式,而且也沒有顯式設置 encapsulation
屬性,那描述符會被設置為 ViewEncapsulation.Emulated
,並被 疏忽見效,運用這類描述符的組件會運用父組件的組件襯着器。
子指令
如今,末了一個題目是,假如我們像下面如許,把一個指令作用在組件模板上,會天生什麼:
<a-comp adir></a-comp>
我們已曉得當為 AComponent
天生工場函數時,編譯器會為 a-comp
元素建立元素定義,會為 AComponent
類建立指令定義。然則因為編譯器會為每一個指令天生指令定義節點,所以上面模板的工場函數像如許(注:Angular v5.* 版本是會為 <a-comp></a-comp>
元素零丁天生一個 *_Host.ngfactory.js
文件,示意宿主視圖,多出來的 jit_directiveDef6(16384, null, 0, jit_ADirective8, [], ...)
是在這個文件代碼里。能夠 ng cli
新建項目檢察 Sources Tab -> ng://
。但作者表達的意義照樣一樣的。):
function View_AppComponent_0() {
return jit_viewDef2(0, [
jit_elementDef3(0, null, null, 2, 'a-comp', [], ...
jit_View_AComponent_04, jit__object_Object_5),
jit_directiveDef6(49152, null, 0, jit_AComponent7, [], ...)
jit_directiveDef6(16384, null, 0, jit_ADirective8, [], ...)
上面代碼都是我們熟習的,僅僅是多增加了一個指令定義,和子組件數目增加為 2。
以上就是悉數了!
注:全文主要講的是組件(視圖)在 Angular 內部是怎樣用指令節點和元素節點定義的。