[譯] 為什麼 Angular 內部沒有發明組件

原文鏈接:
Here is why you will not find components inside Angular

《[譯] 為什麼 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 將會編譯這個模板,並天生兩個元素節點,即 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 元素上。大多數指令運用屬性挑選器(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 版本略有差別):

NameDescription
matchedQueriesused when querying child nodes
childCountspecifies how many children the current element have
ctorreference to the component or directive constructor
depsan array of constructor dependencies
propsan array of input property bindings
outputsan 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 內部是怎樣用指令節點和元素節點定義的。

    原文作者:lx1036
    原文地址: https://segmentfault.com/a/1190000014687993
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞