[譯] 爲什麼 Angular 內部沒有發現組件

原文連接:Here is why you will not find components inside Angularjavascript

Component Not Found

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 將會編譯這個模板,並生成兩個元素節點,即 divh1 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.declarationsmodule.entryComponentscomponent.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-th16-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 內部是如何用指令節點和元素節點定義的。

相關文章
相關標籤/搜索