[譯] Angular 屬性綁定更新機制

原文鏈接:
The mechanics of property bindings update in Angular

《[譯] Angular 屬性綁定更新機制》

一切當代前端框架都是用組件來合成 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 是怎樣曉得 BComponentspan 支撐 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 …

這些學問都很好明白,如今讓我們進一步看看其內部發作了什麼。

組件工場

只管在子組件 BComponentspan 元素綁定了輸入屬性,然則輸入綁定更新所須要的信息悉數在父組件 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 函數重要功能以下:

  1. 從當前視圖節點里獵取組件/指令對象(注:檢察 L156
  2. 搜檢組件/指令對象的綁定屬性值是不是發作轉變(注:檢察 L160-L199
  3. 假如屬性發作轉變:

    a. 假如變動戰略設置為 OnPush,設置視圖狀況為 checksEnabled(注:檢察 L438

    b. 更新子組件的綁定屬性值(注:檢察 L446

    c. 預備 SimpleChange 數據和更新視圖的 oldValues 屬性,新值替代舊值(注:檢察 L451-L454

    d. 挪用生命周期鈎子 ngOnChanges(注:檢察 L201

  4. 假如該視圖是初次實行變動檢測,則挪用生命周期鈎子 ngOnInit(注:檢察 L205
  5. 挪用生命周期鈎子 ngDoCheck(注:檢察 L233

固然,只要在生命周期鈎子在組件內定義了才被挪用,Angular 運用 NodeDef 節點標誌位來推斷是不是有生命周期鈎子,假如檢察源碼你會發明相似以下代碼(注:檢察 L203-L207):

if (... && (def.flags & NodeFlags.OnInit)) {
  directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
  directive.ngDoCheck();
}

和更新元素節點一樣,更新指令時也一樣把上一次的值存儲在視圖數據的屬性 oldValues 里(注:即上面的 3.c 步驟)。

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