原文連接:Here is why you will not find components inside Angularjavascript
Component is just a directive with a template? Or is it?html
從我開始使用 Angular 開始,就被組件和指令間區別的問題所困惑,尤爲對那些從 Angular.js 世界來的人,由於 Angular.js 裏只有指令,儘管咱們也常常把它當作組件來使用。若是你在網上搜這個問題解釋,不少都會這麼解釋(注:爲清晰理解,不翻譯):java
Components are just directives with a content defined in a template…node
Angular components are a subset of directives. Unlike directives, components always have…git
Components are high-order directives with templates and serve as…github
這些說法貌似都對,我在查看由 Angular 編譯器編譯組件生成的視圖工廠源碼裏,的確沒發現組件定義,你若是查看也只會發現 指令。web
注:使用 Angular-CLI ng new 一個新項目,執行 ng serve 運行程序後,就可在 Chrome Dev Tools 的 Source Tab 的 ng:// 域下查看到編譯組件後生成的 **.ngfactory.js 文件,該文件代碼即上面說的視圖工廠源碼。api
可是我在網上沒有找到 緣由解釋,由於想要知道緣由就必須對 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 元素吧。
你可能知道在 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 元素上。大多數指令使用屬性選擇器(property 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 內部是如何用指令節點和元素節點定義的。