摘要:本質上,本文主要解釋內部是如何定義組件和指令的,并引入新的視圖節點定義指令定義。大多數指令使用屬性選擇器,但是有一些也選擇元素選擇器。實際上,表單指令就是使用元素選擇器來把特定行為附著在元素上。但是由于編譯器會為每一個
原文鏈接: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 文本節點 。
所以如果你寫了如下的一個模板:
Hello {{name}}
Angular Compiler 將會編譯這個模板,并生成兩個元素節點,即 div 和 h1 DOM 元素,和一個文本節點,即 Hello {{name}} DOM 文本。這些都是很重要的節點,因為沒有它們,你在屏幕上看不到任何東西。但是組件合成模式告訴我們可以嵌套組件,所以必然另一種視圖節點來嵌入組件。為了搞清楚這些特殊節點是什么,首先需要了解組件是由什么組成的。本質上,組件本質上是具有特定行為的 DOM 元素,而這些行為是在組件類里實現的。首先看下 DOM 元素吧。
自定義 DOM 元素你可能知道在 html 里可以創建一個新的 HTML 標簽,比如,如果不使用框架,你可以直接在 html 里插入一個新的標簽:
然后查詢這個 DOM 節點并檢查類型,你會發現它是個完全合法的 DOM 元素(注:你可以在一個 html 文件里試試這部分代碼,甚至可以寫上
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: "I am A component"
})
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 是一個組件,所以對于下面的簡單模板:
I am A component
編譯器會編譯它,生成一個帶有視圖定義和視圖節點的工廠函數:
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 的視圖存儲在為
注:這段由于涉及大量的源碼函數,會比較晦澀。作者講的是創建視圖的具體過程,細致到很多函數的調用。總之,只需要記住一點就行:視圖解析器通過解析視圖工廠(ViewDefinitionFactory)得到視圖(ViewDefinition)。細節暫不用管。組件渲染器類型拿到了視圖,又該如何畫出來呢?看下文。
Angular 根據組件裝飾器中定義的 ViewEncapsulation 模式來決定使用哪種 DOM 渲染器:
Emulated Encapsulation Renderer
Shadow Renderer
Default Renderer
以上組件渲染器是通過 DomRendererFactory2 來創建的。componentRendererType 參數是在元素定義里被傳入的,本例即是 jit__object_Object_5(注:上面代碼里有這個對象,是 jit_elementDef3() 的最后一個參數),該參數是渲染器的一個基本描述符,用來決定使用哪一個渲染器渲染組件。其中,最重要的是視圖封裝模式和所用于組件的樣式(注:componentRendererType 參數的結構是 RendererType2):
{ styles:[["h1[_ngcontent-%COMP%] {color: green}"]], encapsulation:0 }
如果你為組件定義了樣式,編譯器會自動設置組件的封裝模式為 ViewEncapsulation.Emulated,或者你可以在組件裝飾器里顯式設置 encapsulation 屬性。如果沒有設置任何樣式,并且也沒有顯式設置 encapsulation 屬性,那描述符會被設置為 ViewEncapsulation.Emulated,并被 忽略生效,使用這種描述符的組件會使用父組件的組件渲染器。
子指令現在,最后一個問題是,如果我們像下面這樣,把一個指令作用在組件模板上,會生成什么:
我們已經知道當為 AComponent 生成工廠函數時,編譯器會為 a-comp 元素創建元素定義,會為 AComponent 類創建指令定義。但是由于編譯器會為每一個指令生成指令定義節點,所以上面模板的工廠函數像這樣(注:Angular v5.* 版本是會為
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 內部是如何用指令節點和元素節點定義的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/107752.html
摘要:本文主要介紹輸入輸出綁定方式,特別是當父組件輸入綁定值變化時,如何更新子組件輸入值。更新指令的屬性上文中已經描述了函數是用來更新元素的屬性,而是用來更新子組件的輸入綁定屬性,并且變更檢測期間傳入的參數就是函數。 原文鏈接:The mechanics of property bindings update in Angular showImg(https://segmentfault....
摘要:但如果一個組件在生命周期鉤子里改變父組件屬性,卻是可以的,因為這個鉤子函數是在更新父組件屬性變化之前調用的注即第步,在第步之前調用。 原文鏈接:Angular.js’ $digest is reborn in the newer version of Angular showImg(https://segmentfault.com/img/remote/146000001468785...
摘要:本文將解釋引起這個錯誤的內在原因,檢測機制的內部原理,提供導致這個錯誤的共同行為,并給出修復這個錯誤的解決方案。這一次過程稱為。這個程序設計為子組件拋出一個事件,而父組件監聽這個事件,而這個事件會引起父組件屬性值發生改變。 原文鏈接:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedE...
摘要:大多數初學者會認為也有封裝規則,但實際上沒有。第二個規則是最后導入模塊的,會覆蓋前面導入模塊的。 原文鏈接:Avoiding common confusions with modules in Angular showImg(https://segmentfault.com/img/remote/1460000015298243?w=270&h=360); Angular Modul...
摘要:所以,單向數據流的意思是指在變更檢測期間屬性綁定變更的架構。相反,輸出綁定過程并沒有在變更檢測期間內運行,所以它沒有把單向數據流轉變為雙向數據流。說的單向數據流說的是服務層,而不是視圖層嗷。 原文鏈接: Do you really know what unidirectional data flow means in?Angular 關于單向數據流,還可以參考這篇文章,且文中還有 y...
閱讀 1856·2021-11-22 15:24
閱讀 1312·2021-11-12 10:36
閱讀 3211·2021-09-28 09:36
閱讀 1842·2021-09-02 15:15
閱讀 2754·2019-08-30 15:54
閱讀 2397·2019-08-30 11:02
閱讀 2396·2019-08-29 13:52
閱讀 3545·2019-08-26 11:53