[譯] Angular DOM 更新機制

原文鏈接:
The mechanics of DOM updates in Angular

《[譯] Angular DOM 更新機制》

由模子變化觸發的 DOM 更新是一切前端框架的重要功用(注:即堅持 model 和 view 的同步),固然 Angular 也不破例。定義一個以下模板表達式:

<span>Hello {{name}}</span>

或許相似下面的屬性綁定(注:這與上面代碼等價):

<span [textContent]="'Hello ' + name"></span>

當每次 name 值發生變化時,Angular 會奇異般的自動更新 DOM 元素(注:最上面代碼是更新 DOM 文本節點,上面代碼是更新 DOM 元素節點,二者是不一樣的,下文詮釋)。這表面上看起來很簡樸,然則其內部事變相稱龐雜。而且,DOM 更新僅僅是 Angular 變動檢測機制 的一部份,變動檢測機制重要由以下三步構成:

  • DOM updates(注:即本文將要詮釋的內容)
  • child components Input bindings updates
  • query list updates

本文重要探究變動檢測機制的襯着部份(即 DOM updates 部份)。假如你之前也對這個題目很獵奇,能夠繼承讀下去,相對讓你恍然大悟。

在援用相干源碼時,假定順序是以臨盆情勢運轉。讓我們最先吧!

順序內部架構

在探究 DOM 更新之前,我們先搞清楚 Angular 順序內部究竟是怎樣設想的,簡樸回憶下吧。

視圖

從我的這篇文章 Here is what you need to know about dynamic components in Angular 曉得 Angular 編譯器會把順序中運用的組件編譯為一個工場類(factory)。比方,下面代碼展現 Angular 怎樣從工場類中建立一個組件(注:這裏作者邏輯貌似有點亂,前一句說的 Angular 編譯器編譯的工場類,現實上是編譯器去做的,不須要開發者做任何事變,是自動化的事變;而下面代碼說的是開發者怎樣手動經由歷程 ComponentFactory 來建立一個 Component 實例。總之,他是想說組件是怎樣被實例化的):

const factory = r.resolveComponentFactory(AComponent);
componentRef: ComponentRef<AComponent> = factory.create(injector);

Angular 運用這個工場類來實例化 View Definition ,然後運用 viewDef 函數來 建立視圖。Angular 內部把一個順序看作為一顆視圖樹,一個順序雖然有浩瀚組件,但有一個大眾的視圖定義接口來定義由組件天生的視圖構造(注:即 ViewDefinition Interface),固然 Angular 運用每個組件對象來建立對應的視圖,從而由多個視圖構成視圖樹。(注:這裡有一個重要觀點就是視圖,其構造就是 ViewDefinition Interface

組件工場

組件工場大部份代碼是由編譯器天生的差別視圖節點構成的,這些視圖節點是經由歷程模板剖析天生的(注:編譯器天生的組件工場是一個返回值為函數的函數,上文的 ComponentFactory 是 Angular 供應的類,供手動挪用。固然,二者指向同一個事物,只是表現情勢差別罷了)。假定定義一個組件的模板以下:

<span>I am {{name}}</span>

編譯器會剖析這個模板天生包括以下相似的組件工場代碼(注:這隻是最重要的部份代碼):

function View_AComponent_0(l) {
    return jit_viewDef1(0,
        [
          jit_elementDef2(0,null,null,1,'span',...),
          jit_textDef3(null,['I am ',...])
        ], 
        null,
        function(_ck,_v) {
            var _co = _v.component;
            var currVal_0 = _co.name;
            _ck(_v,1,0,currVal_0);

注:由 AppComponent 組件編譯天生的工場函數完全代碼以下

 (function(jit_createRendererType2_0,jit_viewDef_1,jit_elementDef_2,jit_textDef_3) {
     var styles_AppComponent = [''];
     var RenderType_AppComponent = jit_createRendererType2_0({encapsulation:0,styles:styles_AppComponent,data:{}});
     function View_AppComponent_0(_l) {
         return jit_viewDef_1(0,
            [
                (_l()(),jit_elementDef_2(0,0,null,null,1,'span',[],null,null,null,null,null)),
                (_l()(),jit_textDef_3(1,null,['I am ','']))
            ],
            null,
            function(_ck,_v) {
                var _co = _v.component;
                var currVal_0 = _co.name;
                _ck(_v,1,0,currVal_0);
           });
    }
 return {RenderType_AppComponent:RenderType_AppComponent,View_AppComponent_0:View_AppComponent_0};})

上面代碼形貌了視圖的構造,並在實例化組件時會被挪用。jit_viewDef_1 實在就是 viewDef 函數,用來建立視圖(注:viewDef 函數很重要,由於視圖是挪用它建立的,天生的視圖構造等於 ViewDefinition)。

viewDef 函數的第二個參數 nodes 有些相似 html 中節點的意義,但卻不僅僅如此。上面代碼中第二個參數是一個數組,其第一個數組元素 jit_elementDef_2 是元素節點定義,第二個數組元素 jit_textDef_3 是文本節點定義。Angular 編譯器會天生許多差別的節點定義,節點範例是由 NodeFlags 設置的。稍後我們將看到 Angular 怎樣依據差別節點範例來做 DOM 更新。

本文只對元素和文本節點感興趣:

export const enum NodeFlags {
    TypeElement = 1 << 0, 
    TypeText = 1 << 1

讓我們扼要擼一遍。

注:上文作者說了一大段,實在中心就是,
順序是一堆視圖構成的,而每個視圖又是由差別範例節點構成的。而本文只體貼元素節點和文本節點,至於另有個重要的指令節點在另一篇文章。

元素節點的構造定義

元素節點構造 是 Angular 編譯每個 html 元素天生的節點構造,它也是用來天生組件的,如對這點感興趣可檢察 Here is why you will not find components inside Angular。元素節點也能夠包括其他元素節點和文本節點作為子節點,子節點數目是由 childCount 設置的。

一切元素定義是由 elementRef 函數天生的,而工場函數中的 jit_elementDef_2() 就是這個函數。elementRef() 重要有以下幾個一般性參數:

NameDescription
childCountspecifies how many children the current element have
namespaceAndNamethe name of the html element(注:如 ‘span’)
fixedAttrsattributes defined on the element

另有其他的幾個具有特定機能的參數:

NameDescription
matchedQueriesDslused when querying child nodes
ngContentIndexused for node projection
bindingsused for dom and bound properties update
outputs, handleEventused for event propagation

本文重要對 bindings 感興趣。

注:從上文曉得視圖(view)是由差別範例節點(nodes)構成的,而元素節點(element nodes)是由
elementRef 函數天生的,元素節點的構造是由
ElementDef 定義的。

文本節點的構造定義

文本節點構造 是 Angular 編譯每個 html 文本 天生的節點構造。一般它是元素定義節點的子節點,就像我們本文的示例那樣(注:<span>I am {{name}}</span>span 是元素節點,I am {{name}} 是文本節點,也是 span 的子節點)。這個文本節點是由 textDef 函數天生的。它的第二個參數以字符串數組情勢傳進來(注: Angular v5.* 是第三個參數)。比方,下面的文本:

<h1>Hello {{name}} and another {{prop}}</h1>

將要被剖析為一個數組:

["Hello ", " and another ", ""]

然後被用來天生準確的綁定:

{
  text: 'Hello',
  bindings: [
    {
      name: 'name',
      suffix: ' and another '
    },
    {
      name: 'prop',
      suffix: ''
    }
  ]
}

在臟搜檢(注:即變動檢測)階段會這麼用來天生文本:

text
+ context[bindings[0][property]] + context[bindings[0][suffix]]
+ context[bindings[1][property]] + context[bindings[1][suffix]]

注:同上,文本節點是由
textDef 函數天生的,構造是由
TextDef 定義的。既然已曉得了兩個節點的定義和天生,那節點上的屬性綁定, Angular 是怎樣處置懲罰的呢?

節點的綁定

Angular 運用 BindingDef 來定義每個節點的綁定依靠,而這些綁定依靠一般是組件類的屬性。在變動檢測時 Angular 會依據這些綁定來決議怎樣更新節點和供應上下文信息。詳細哪種操縱是由 BindingFlags 決議的,下面列表展現了詳細的 DOM 操縱範例:

NameConstruction in template
TypeElementAttributeattr.name
TypeElementClassclass.name
TypeElementStylestyle.name

元素和文本定義依據這些編譯器可辨認的綁定標誌位,內部建立這些綁定依靠。每一種節點範例都有着差別的綁定天生邏輯(注:意義是 Angular 會依據 BindingFlags 來天生對應的 BindingDef)。

更新襯着器

最使我們感興趣的是 jit_viewDef_1 中末了誰人蔘數:

function(_ck,_v) {
   var _co = _v.component;
   var currVal_0 = _co.name;
   _ck(_v,1,0,currVal_0);
});

這個函數叫做 updateRenderer。它吸收兩個參數:_ck_v_ckcheck 的簡寫,實在就是 prodCheckAndUpdateNode 函數,而 _v 就是當前視圖對象。updateRenderer 函數會在 每一次變動檢測時 被挪用,其參數 _ck_v 也是這時候被傳入。

updateRenderer 函數邏輯重如果,從組件對象的綁定屬性獵取當前值,並挪用 _ck 函數,同時傳入視圖對象、視圖節點索引和綁定屬性當前值。重要一點是 Angular 會為每個視圖實行 DOM 更新操縱,所以必需傳入視圖節點索引參數(注:這個很好明白,上文說了 Angular 會順次對每個 view 做模子視圖同步歷程)。你能夠清楚看到 _ck 參數列表:

function prodCheckAndUpdateNode(
    view: ViewData, 
    nodeIndex: number, 
    argStyle: ArgumentType, 
    v0?: any, 
    v1?: any, 
    v2?: any,

nodeIndex 是視圖節點的索引,假如你模板中有多個表達式:

<h1>Hello {{name}}</h1>
<h1>Hello {{age}}</h1>

編譯器天生的 updateRenderer 函數以下:

var _co = _v.component;

// here node index is 1 and property is `name`
var currVal_0 = _co.name;
_ck(_v,1,0,currVal_0);

// here node index is 4 and bound property is `age`
var currVal_1 = _co.age;
_ck(_v,4,0,currVal_1);

更新 DOM

如今我們已曉得 Angular 編譯器天生的一切對象(注:已有了 view,element node,text node 和 updateRenderer 這幾個道具),如今我們能夠探究怎樣運用這些對象來更新 DOM。

從上文我們曉得變動檢測時期 updateRenderer 函數傳入的一個參數是 _ck 函數,而這個函數就是 prodCheckAndUpdateNode。這個函數在繼承實行后,終究會挪用 checkAndUpdateNodeInline ,假如綁定屬性的數目凌駕 10,Angular 還供應了 checkAndUpdateNodeDynamic 這個函數(注:兩個函數實質一樣)。

checkAndUpdateNodeInline 函數會依據差別視圖節點範例來實行對應的搜檢更新函數:

case NodeFlags.TypeElement   -> checkAndUpdateElementInline
case NodeFlags.TypeText      -> checkAndUpdateTextInline
case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline

讓我們看下這些函數是做什麼的,至於 NodeFlags.TypeDirective 能夠檢察我寫的文章 The mechanics of property bindings update in Angular

注:由於本文只關注
element node 和 text node

元素節點

關於元素節點,會挪用函數 checkAndUpdateElementInline 以及 checkAndUpdateElementValuecheckAndUpdateElementValue 函數會搜檢綁定情勢是不是是 [attr.name, class.name, style.some] 或是屬性綁定情勢:

case BindingFlags.TypeElementAttribute -> setElementAttribute
case BindingFlags.TypeElementClass     -> setElementClass
case BindingFlags.TypeElementStyle     -> setElementStyle
case BindingFlags.TypeProperty         -> setElementProperty;

然後運用襯着器對應的方法來對該節點實行對應操縱,比方運用 setElementClass 給當前節點 span 增加一個 class

文本節點

關於文本節點範例,會挪用 checkAndUpdateTextInline ,下面是重要部份:

if (checkAndUpdateBinding(view, nodeDef, bindingIndex, newValue)) {
    value = text + _addInterpolationPart(...);
    view.renderer.setValue(DOMNode, value);
}

它會拿到 updateRenderer 函數傳過來的當前值(注:即上文的 _ck(_v,4,0,currVal_1);),與上一次變動檢測時的值相比較。視圖數據包括有 oldValues 屬性,假如屬性值如 name 發生變化,Angular 會運用最新 name 值合成最新的字符串文本,如 Hello New World,然後運用襯着器更新 DOM 上對應的文本。

注:更新元素節點和文本節點都提到了襯着器(renderer),這也是一個重要的觀點。每個視圖對象都有一個
renderer 屬性,等於
Renderer2 的援用,也就是組件襯着器,DOM 的現實更新操縱由它完成。由於 Angular 是跨平台的,這個 Renderer2 是個接口,如許依據差別 Platform 就挑選差別的 Renderer。比方,在瀏覽器里這個 Renderer 就是 DOMRenderer,在服務端就是 ServerRenderer,等等。
從這裏可看出,Angular 框架設想做了很好的籠統。

結論

我曉得有大批難明的信息須要消化,然則只需明白了這些學問,你就能夠更好的設想順序或許去調試 DOM 更新相干的題目。我發起你根據本文提到的源碼邏輯,運用調試器或 debugger 語句 一步步去調試源碼。

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