一切當代前端框架都是用組件來合成 UI,如許很天然就會發作父子組件層級,這就須要框架供應父子組件通訊的機制。一樣,Angular 也供應了兩種體式格局來完成父子組件通訊:輸入輸出綁定和同享效勞。關於 stateless presentational components 我更喜好輸入輸出綁定體式格局,然則關於 stateful container components 我運用同享效勞體式格局。
本文重要引見輸入輸出綁定體式格局,特別是當父組件輸入綁定值變化時,Angular 怎樣更新子組件輸入值。假如想相識 Angular 怎樣更新當前組件 DOM,能夠檢察 譯 Angular DOM 更新機制,這篇文章也會有助於加深對本文的明白。由於我們將探究 Angular 怎樣更新 DOM 元素和組件的輸入綁定屬性,所以假定你曉得 Angular 內部是怎樣表現組件和指令的,假如你不是很相識而且很感興趣,能夠檢察 譯 為什麼 Angular 內部沒有發明組件, 這篇文章重要講了 Angular 內部怎樣運用指令情勢來示意組件。而本文關於組件和指令兩個觀點交換運用,由於 Angular 內部就是把組件當作指令。
模板綁定語法
你能夠曉得 Angular 供應了 屬性綁定語法 —— []
,這個語法很通用,它能夠用在子組件上,也能夠用在原生 DOM 元素上。假如你想從父組件把數據傳給子組件 b-comp
或許原生 DOM 元素 span
,你能夠在父組件模板中這麼寫:
import { Component } from '@angular/core';
@Component({
moduleId: module.id,
selector: 'a-comp',
template: `
<b-comp [textContent]="AText"></b-comp>
<span [textContent]="AText"></span>
`
})
export class AComponent {
AText = 'some';
}
你沒必要為原生 DOM 元素做些分外的事情,然則關於子組件 b-comp
你須要說明輸入屬性 textContent
:
@Component({
selector: 'b-comp',
template: 'Comes from parent: {{textContent}}'
})
export class BComponent {
@Input() textContent;
}
如許當父組件 AComponent.AText
屬性轉變時,Angular 會自動更新子組件 BComponent.textContent
屬性,和原生元素 span.textContent
屬性。同時,還會挪用子組件 BComponent
的生命周期鈎子函數 ngOnChanges
(注:實際上另有 ngDoCheck
,見下文)。
你能夠獵奇 Angular 是怎樣曉得 BComponent
和 span
支撐 textContent
綁定的。這是由於 Angular 編譯器在剖析模板時,假如碰到簡樸 DOM 元素如 span
,就去查找這個元素是不是定義在 dom_element_schema_registry,從而曉得它是 HTMLElement 子類,textContent
是个中的一個屬性(注:能夠嘗嘗假如 span
綁定一個 [abc]=AText
就報錯,沒法辨認 abc
屬性);假如碰到了組件或指令,就去檢察其裝潢器 @Component/@Directive
的元數據 input
屬性里是不是有該綁定屬性項,假如沒有,編譯器一樣會拋出毛病:
Can’t bind to ‘textContent’ since it isn’t a known property of …
這些學問都很好明白,如今讓我們進一步看看其內部發作了什麼。
組件工場
只管在子組件 BComponent
和 span
元素綁定了輸入屬性,然則輸入綁定更新所須要的信息悉數在父組件 AComponent
的組件工場里。讓我們看下 AComponent
的組件工場代碼:
function View_AComponent_0(_l) {
return jit_viewDef1(0, [
jit_elementDef_2(..., 'b-comp', ...),
jit_directiveDef_5(..., jit_BComponent6, [], {
textContent: [0, 'textContent']
}, ...),
jit_elementDef_2(..., 'span', [], [[8, 'textContent', 0]], ...)
], function (_ck, _v) {
var _co = _v.component;
var currVal_0 = _co.AText;
var currVal_1 = 'd';
_ck(_v, 1, 0, currVal_0, currVal_1);
}, function (_ck, _v) {
var _co = _v.component;
var currVal_2 = _co.AText;
_ck(_v, 2, 0, currVal_2);
});
}
假如你讀了 譯 Angular DOM 更新機制 或 譯 為什麼 Angular 內部沒有發明組件,就會對上面代碼中的各個視圖節點比較熟習了。前兩個節點中,jit_elementDef_2
是元素節點,jit_directiveDef_5
是指令節點,這兩個構成了子組件 BComponent
;第三個節點 jit_elementDef_2
也是元素節點,構成了 span
元素。
節點綁定
雷同範例的節點運用雷同的節點定義函數,但區別是吸收的參數差別,比方 jit_directiveDef_5
節點定義函數參數以下:
jit_directiveDef_5(..., jit_BComponent6, [], {
textContent: [0, 'textContent']
}, ...),
个中,參數 {textContent: [0, 'textContent']}
叫做 props,這點能夠檢察 directiveDef 函數的參數列表:
directiveDef(..., props?: {[name: string]: [number, string]}, ...)
props 參數是一個對象,每個鍵為綁定屬性名,對應的值為綁定索引和綁定屬性名構成的數組,比方本例中只要一個綁定,textContent 對應的值為:
{textContent: [0, 'textContent']}
假如指令有多個綁定,比方:
<b-comp [textContent]="AText" [otherProp]="AProp">
props 參數值也包括兩個屬性:
jit_directiveDef5(49152, null, 0, jit_BComponent6, [], {
textContent: [0, 'textContent'],
otherProp: [1, 'otherProp']
}, null),
Angular 會運用這些值來天生當前指令節點的 binding,從而天生當前視圖的指令節點。在變動檢測時,每個 binding 決議 Angular 運用哪一種操縱來更新節點和供應上下文信息,綁定範例是經由過程 BindingFlags 設置的(注:每個綁定定義是 BindingDef,它的屬性 flags: BindingFlags 決議 Angular 該採用什麼操縱,比方 Class
型綁定和 Style
型綁定都邑挪用對應的操縱函數,見下文)。比方,假如是屬性綁定,編譯器會設置綁定標誌位為:
export const enum BindingFlags {
TypeProperty = 1 << 3,
注:上文說完了指令定義函數的參數,下面說說元素定義函數的參數。
本例中,由於 span
元素有屬性綁定,編譯器會設置綁定參數為 [[8, 'textContent', 0]]
:
jit_elementDef2(..., 'span', [], [[8, 'textContent', 0]], ...)
差別於指令節點,對元素節點來講,綁定參數構造是個二維數組,由於 span
元素只要一個綁定,所以它僅僅只要一個子數組。數組 [8, 'textContent', 0]
中第一個參數也一樣是綁定標誌位 BindingFlags,決議 Angular 應當採用什麼範例操縱(注:[8, 'textContent', 0]
中的 8
示意為 property
型綁定):
export const enum BindingFlags {
TypeProperty = 1 << 3, // 8
其他範例標誌位已在文章 譯 Angular DOM 更新機制 有所詮釋:
TypeElementAttribute = 1 << 0,
TypeElementClass = 1 << 1,
TypeElementStyle = 1 << 2,
編譯器不會為指令定義供應綁定標誌位,由於指令的綁定範例也只能是 BindingFlags.TypeProperty
。
注:
節點綁定 這一節重要講的是關於元素節點來講,每個節點的
binding 範例是由
BindingFlags 決議的;關於指令節點來講,每個節點的
binding 範例只能是
BindingFlags.TypeProperty
。
updateRenderer 和 updateDirectives
組件工場代碼里,編譯器還為我們天生了兩個函數:
function (_ck, _v) {
var _co = _v.component;
var currVal_0 = _co.AText;
var currVal_1 = _co.AProp;
_ck(_v, 1, 0, currVal_0, currVal_1);
},
function (_ck, _v) {
var _co = _v.component;
var currVal_2 = _co.AText;
_ck(_v, 2, 0, currVal_2);
}
假如你讀了 譯 Angular DOM 更新機制,應當對第二個函數即 updateRenderer 有所熟習。第一個函數叫做 updateDirectives。這兩個函數都是 ViewUpdateFn 範例接口,二者都是視圖定義的屬性:
interface ViewDefinition {
flags: ViewFlags;
updateDirectives: ViewUpdateFn;
updateRenderer: ViewUpdateFn;
風趣的是這兩個函數的函數體基礎雷同,參數都是 _ck
和 _v
,而且兩個函數的對應參數都指向同一個對象,所以為什麼須要兩個函數?
由於在變動檢測時期,這是差別階段的兩個差別行動:
這兩個操縱是在變動檢測的差別階段實行,所以 Angular 須要兩個自力的函數離別在對應的階段挪用:
- updateDirectives——變動檢測的最先階段被挪用,來更新子組件的輸入綁定屬性
- updateRenderer——變動檢測的中心階段被挪用,來更新當前組件的 DOM 元素
這兩個函數都邑在 Angular 每次的變動檢測時 被挪用,而且函數參數也是在這時候被傳入的。讓我們看看函數內部做了哪些事情。
_ck
就是 check
的縮寫,實在就是函數 prodCheckAndUpdateNode,另一個參數就是 組件視圖數據。函數的重要功能就是從組件對象里拿到綁定屬性的當前值,然後和視圖數據對象、視圖節點索引等一同傳入 prodCheckAndUpdateNode 函數。个中,由於 Angular 會更新每個視圖的 DOM,所以須要傳入當前視圖的索引。假如我們有兩個 span
和兩個組件:
<b-comp [textContent]="AText"></b-comp>
<b-comp [textContent]="AText"></b-comp>
<span [textContent]="AText"></span>
<span [textContent]="AText"></span>
編譯器天生的 updateRenderer
函數和 updateDirectives
函數以下:
function(_ck, _v) {
var _co = _v.component;
var currVal_0 = _co.AText;
// update first component
_ck(_v, 1, 0, currVal_0);
var currVal_1 = _co.AText;
// update second component
_ck(_v, 3, 0, currVal_1);
},
function(_ck, _v) {
var _co = _v.component;
var currVal_2 = _co.AText;
// update first span
_ck(_v, 4, 0, currVal_2);
var currVal_3 = _co.AText;
// update second span
_ck(_v, 5, 0, currVal_3);
}
沒有什麼更龐雜的東西,這兩個函數還不是重點,重點是 _ck
函數,接着往下看。
更新元素的屬性
從上文我們曉得,編譯器天生的 updateRenderer
函數會在每一次變動檢測被挪用,用來更新 DOM 元素的屬性,而且其參數 _ck
就是函數 prodCheckAndUpdateNode。關於 DOM 元素的更新,該函數經由一系列的函數挪用后,終究會挪用函數 checkAndUpdateElementValue,這個函數會搜檢綁定標誌位是 [attr.name, class.name, style.some]
个中的哪個,又或許是屬性綁定(注:可檢察源碼這段 L233-L250):
case BindingFlags.TypeElementAttribute -> setElementAttribute
case BindingFlags.TypeElementClass -> setElementClass
case BindingFlags.TypeElementStyle -> setElementStyle
case BindingFlags.TypeProperty -> setElementProperty;
上面代碼就是方才說的幾個綁定範例,當綁定標誌位是 BindingFlags.TypeProperty
,會挪用函數 setElementProperty,該函數內部也是經由過程挪用 DOM Renderer 的 setProperty 方法來更新 DOM。
注:
setElementProperty
函數里這行代碼
view.renderer.setProperty(renderNode,name, renderValue);
,renderer 就是
Renderer2 interface,它僅僅是一個接口,在瀏覽器平台下,它的完成就是
DefaultDomRenderer2。
更新指令的屬性
上文中已形貌了 updateRenderer
函數是用來更新元素的屬性,而 updateDirective
是用來更新子組件的輸入綁定屬性,而且變動檢測時期傳入的參數 _ck
就是函數 prodCheckAndUpdateNode。只是進過一系列函數挪用后,終究挪用的函數倒是checkAndUpdateDirectiveInline,這是由於此次節點的標誌位是 NodeFlags.TypeDirective
(注:可檢察源碼 L428-L429),checkAndUpdateDirectiveInline 函數重要功能以下:
- 從當前視圖節點里獵取組件/指令對象(注:檢察 L156)
- 搜檢組件/指令對象的綁定屬性值是不是發作轉變(注:檢察 L160-L199)
- 假如屬性發作轉變:
a. 假如變動戰略設置為
OnPush
,設置視圖狀況為checksEnabled
(注:檢察 L438)b. 更新子組件的綁定屬性值(注:檢察 L446)
c. 預備
SimpleChange
數據和更新視圖的oldValues
屬性,新值替代舊值(注:檢察 L451-L454)d. 挪用生命周期鈎子 ngOnChanges(注:檢察 L201)
- 假如該視圖是初次實行變動檢測,則挪用生命周期鈎子 ngOnInit(注:檢察 L205)
- 挪用生命周期鈎子 ngDoCheck(注:檢察 L233)
固然,只要在生命周期鈎子在組件內定義了才被挪用,Angular 運用 NodeDef 節點標誌位來推斷是不是有生命周期鈎子,假如檢察源碼你會發明相似以下代碼(注:檢察 L203-L207):
if (... && (def.flags & NodeFlags.OnInit)) {
directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
directive.ngDoCheck();
}
和更新元素節點一樣,更新指令時也一樣把上一次的值存儲在視圖數據的屬性 oldValues 里(注:即上面的 3.c
步驟)。