溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Angular DOM中更新機(jī)制的示例分析

發(fā)布時間:2021-05-27 11:34:24 來源:億速云 閱讀:242 作者:小新 欄目:web開發(fā)

這篇文章主要介紹了Angular DOM中更新機(jī)制的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

Angular DOM中更新機(jī)制的示例分析

由模型變化觸發(fā)的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 view 的同步),當(dāng)然 Angular 也不例外。定義一個如下模板表達(dá)式:

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

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

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

當(dāng)每次 name 值發(fā)生變化時,Angular 會神奇般的自動更新 DOM 元素(注:最上面代碼是更新 DOM 文本節(jié)點,上面代碼是更新 DOM 元素節(jié)點,兩者是不一樣的,下文解釋)。這表面上看起來很簡單,但是其內(nèi)部工作相當(dāng)復(fù)雜。而且,DOM 更新僅僅是 Angular 變更檢測機(jī)制 的一部分,變更檢測機(jī)制主要由以下三步組成:

  • DOM updates(注:即本文將要解釋的內(nèi)容)

  • child components Input bindings updates

  • query list updates

本文主要探索變更檢測機(jī)制的渲染部分(即 DOM updates 部分)。如果你之前也對這個問題很好奇,可以繼續(xù)讀下去,絕對讓你茅塞頓開。

在引用相關(guān)源碼時,假設(shè)程序是以生產(chǎn)模式運行。讓我們開始吧!

程序內(nèi)部架構(gòu)

在探索 DOM 更新之前,我們先搞清楚 Angular 程序內(nèi)部究竟是如何設(shè)計的,簡單回顧下吧。

視圖

從我的這篇文章 Here is what you need to know about dynamic components in Angular 知道 Angular 編譯器會把程序中使用的組件編譯為一個工廠類(factory)。例如,下面代碼展示 Angular 如何從工廠類中創(chuàng)建一個組件(注:這里作者邏輯貌似有點亂,前一句說的 Angular 編譯器編譯的工廠類,其實是編譯器去做的,不需要開發(fā)者做任何事情,是自動化的事情;而下面代碼說的是開發(fā)者如何手動通過 ComponentFactory 來創(chuàng)建一個 Component 實例??傊?,他是想說組件是怎么被實例化的):

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

Angular 使用這個工廠類來實例化 View Definition ,然后使用 viewDef 函數(shù)來 創(chuàng)建視圖。Angular 內(nèi)部把一個程序看作為一顆視圖樹,一個程序雖然有眾多組件,但有一個公共的視圖定義接口來定義由組件生成的視圖結(jié)構(gòu)(注:即 ViewDefinition Interface),當(dāng)然 Angular 使用每一個組件對象來創(chuàng)建對應(yīng)的視圖,從而由多個視圖組成視圖樹。(注:這里有一個主要概念就是視圖,其結(jié)構(gòu)就是  ViewDefinition Interface

組件工廠

組件工廠大部分代碼是由編譯器生成的不同視圖節(jié)點組成的,這些視圖節(jié)點是通過模板解析生成的(注:編譯器生成的組件工廠是一個返回值為函數(shù)的函數(shù),上文的 ComponentFactory 是 Angular 提供的類,供手動調(diào)用。當(dāng)然,兩者指向同一個事物,只是表現(xiàn)形式不同而已)。假設(shè)定義一個組件的模板如下:

<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 組件編譯生成的工廠函數(shù)完整代碼如下

 (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};})

上面代碼描述了視圖的結(jié)構(gòu),并在實例化組件時會被調(diào)用。jit_viewDef_1 其實就是 viewDef 函數(shù),用來創(chuàng)建視圖(注:viewDef 函數(shù)很重要,因為視圖是調(diào)用它創(chuàng)建的,生成的視圖結(jié)構(gòu)即是 ViewDefinition)。

viewDef 函數(shù)的第二個參數(shù) nodes 有些類似 html 中節(jié)點的意思,但卻不僅僅如此。上面代碼中第二個參數(shù)是一個數(shù)組,其第一個數(shù)組元素 jit_elementDef_2 是元素節(jié)點定義,第二個數(shù)組元素 jit_textDef_3 是文本節(jié)點定義。Angular 編譯器會生成很多不同的節(jié)點定義,節(jié)點類型是由 NodeFlags 設(shè)置的。稍后我們將看到 Angular 如何根據(jù)不同節(jié)點類型來做 DOM 更新。

本文只對元素和文本節(jié)點感興趣:

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

讓我們簡要擼一遍。

注:上文作者說了一大段,其實核心就是,程序是一堆視圖組成的,而每一個視圖又是由不同類型節(jié)點組成的。而本文只關(guān)心元素節(jié)點和文本節(jié)點,至于還有個重要的指令節(jié)點在另一篇文章。

元素節(jié)點的結(jié)構(gòu)定義

元素節(jié)點結(jié)構(gòu) 是 Angular 編譯每一個 html 元素生成的節(jié)點結(jié)構(gòu),它也是用來生成組件的,如對這點感興趣可查看 Here is why you will not find components inside Angular。元素節(jié)點也可以包含其他元素節(jié)點和文本節(jié)點作為子節(jié)點,子節(jié)點數(shù)量是由 childCount 設(shè)置的。

所有元素定義是由 elementRef 函數(shù)生成的,而工廠函數(shù)中的 jit_elementDef_2() 就是這個函數(shù)。elementRef() 主要有以下幾個一般性參數(shù):

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

還有其他的幾個具有特定性能的參數(shù):

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

本文主要對 bindings 感興趣。

注:從上文知道視圖(view)是由不同類型節(jié)點(nodes)組成的,而元素節(jié)點(element nodes)是由 elementRef 函數(shù)生成的,元素節(jié)點的結(jié)構(gòu)是由 ElementDef 定義的。

文本節(jié)點的結(jié)構(gòu)定義

文本節(jié)點結(jié)構(gòu) 是 Angular 編譯每一個 html 文本 生成的節(jié)點結(jié)構(gòu)。通常它是元素定義節(jié)點的子節(jié)點,就像我們本文的示例那樣(注:<span>I am {{name}}</span>,span 是元素節(jié)點,I am {{name}} 是文本節(jié)點,也是 span 的子節(jié)點)。這個文本節(jié)點是由 textDef 函數(shù)生成的。它的第二個參數(shù)以字符串?dāng)?shù)組形式傳進(jìn)來(注: Angular v5.* 是第三個參數(shù))。例如,下面的文本:

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

將要被解析為一個數(shù)組:

["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]]

注:同上,文本節(jié)點是由 textDef 函數(shù)生成的,結(jié)構(gòu)是由 TextDef 定義的。既然已經(jīng)知道了兩個節(jié)點的定義和生成,那節(jié)點上的屬性綁定, Angular 是怎么處理的呢?

節(jié)點的綁定

Angular 使用 BindingDef 來定義每一個節(jié)點的綁定依賴,而這些綁定依賴通常是組件類的屬性。在變更檢測時 Angular 會根據(jù)這些綁定來決定如何更新節(jié)點和提供上下文信息。具體哪一種操作是由 BindingFlags 決定的,下面列表展示了具體的 DOM 操作類型:

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

元素和文本定義根據(jù)這些編譯器可識別的綁定標(biāo)志位,內(nèi)部創(chuàng)建這些綁定依賴。每一種節(jié)點類型都有著不同的綁定生成邏輯(注:意思是 Angular 會根據(jù) BindingFlags 來生成對應(yīng)的 BindingDef)。

更新渲染器

最讓我們感興趣的是 jit_viewDef_1 中最后那個參數(shù):

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

這個函數(shù)叫做 updateRenderer。它接收兩個參數(shù):_ck_v_ckcheck 的簡寫,其實就是 prodCheckAndUpdateNode 函數(shù),而 _v 就是當(dāng)前視圖對象。updateRenderer 函數(shù)會在 每一次變更檢測時 被調(diào)用,其參數(shù) _ck_v 也是這時被傳入。

updateRenderer 函數(shù)邏輯主要是,從組件對象的綁定屬性獲取當(dāng)前值,并調(diào)用 _ck 函數(shù),同時傳入視圖對象、視圖節(jié)點索引和綁定屬性當(dāng)前值。重要一點是 Angular 會為每一個視圖執(zhí)行 DOM 更新操作,所以必須傳入視圖節(jié)點索引參數(shù)(注:這個很好理解,上文說了 Angular 會依次對每一個 view 做模型視圖同步過程)。你可以清晰看到 _ck 參數(shù)列表:

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

nodeIndex 是視圖節(jié)點的索引,如果你模板中有多個表達(dá)式:

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

編譯器生成的 updateRenderer 函數(shù)如下:

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

現(xiàn)在我們已經(jīng)知道 Angular 編譯器生成的所有對象(注:已經(jīng)有了 view,element node,text node 和 updateRenderer 這幾個道具),現(xiàn)在我們可以探索如何使用這些對象來更新 DOM。

從上文我們知道變更檢測期間 updateRenderer 函數(shù)傳入的一個參數(shù)是 _ck 函數(shù),而這個函數(shù)就是 prodCheckAndUpdateNode。這個函數(shù)在繼續(xù)執(zhí)行后,最終會調(diào)用 checkAndUpdateNodeInline ,如果綁定屬性的數(shù)量超過 10,Angular 還提供了 checkAndUpdateNodeDynamic 這個函數(shù)(注:兩個函數(shù)本質(zhì)一樣)。

checkAndUpdateNodeInline 函數(shù)會根據(jù)不同視圖節(jié)點類型來執(zhí)行對應(yīng)的檢查更新函數(shù):

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

讓我們看下這些函數(shù)是做什么的,至于 NodeFlags.TypeDirective 可以查看我寫的文章 The mechanics of property bindings update in Angular

注:因為本文只關(guān)注 element node 和 text node。

元素節(jié)點

對于元素節(jié)點,會調(diào)用函數(shù) checkAndUpdateElementInline 以及 checkAndUpdateElementValue,checkAndUpdateElementValue 函數(shù)會檢查綁定形式是否是 [attr.name, class.name, style.some] 或是屬性綁定形式:

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

然后使用渲染器對應(yīng)的方法來對該節(jié)點執(zhí)行對應(yīng)操作,比如使用 setElementClass 給當(dāng)前節(jié)點 span 添加一個 class。

文本節(jié)點

對于文本節(jié)點類型,會調(diào)用 checkAndUpdateTextInline ,下面是主要部分:

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

它會拿到 updateRenderer 函數(shù)傳過來的當(dāng)前值(注:即上文的 _ck(_v,4,0,currVal_1);),與上一次變更檢測時的值相比較。視圖數(shù)據(jù)包含有 oldValues 屬性,如果屬性值如 name 發(fā)生變化,Angular 會使用最新 name 值合成最新的字符串文本,如 Hello New World,然后使用渲染器更新 DOM 上對應(yīng)的文本。

注:更新元素節(jié)點和文本節(jié)點都提到了渲染器(renderer),這也是一個重要的概念。每一個視圖對象都有一個 renderer 屬性,即是 Renderer2 的引用,也就是組件渲染器,DOM 的實際更新操作由它完成。因為 Angular 是跨平臺的,這個 Renderer2 是個接口,這樣根據(jù)不同 Platform 就選擇不同的 Renderer。比如,在瀏覽器里這個 Renderer 就是 DOMRenderer,在服務(wù)端就是 ServerRenderer,等等。從這里可看出,Angular 框架設(shè)計做了很好的抽象。

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“Angular DOM中更新機(jī)制的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識等著你來學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI