您好,登錄后才能下訂單哦!
這篇文章主要介紹了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()
主要有以下幾個一般性參數:
Name | Description |
---|---|
childCount | specifies how many children the current element have |
namespaceAndName | the name of the html element(注:如 'span') |
fixedAttrs | attributes defined on the element |
還有其他的幾個具有特定性能的參數:
Name | Description |
---|---|
matchedQueriesDsl | used when querying child nodes |
ngContentIndex | used for node projection |
bindings | used for dom and bound properties update |
outputs, handleEvent | used 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.* 是第三個參數)。例如,下面的文本:
<h2>Hello {{name}} and another {{prop}}</h2>
將要被解析為一個數組:
["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 操作類型:
Name | Construction in template |
---|---|
TypeElementAttribute | attr.name |
TypeElementClass | class.name |
TypeElementStyle | style.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
。_ck
是 check
的簡寫,其實就是 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
是視圖節點的索引,如果你模板中有多個表達式:
<h2>Hello {{name}}</h2> <h2>Hello {{age}}</h2>
編譯器生成的 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);
現在我們已經知道 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 以及 checkAndUpdateElementValue,checkAndUpdateElementValue
函數會檢查綁定形式是否是 [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 框架設計做了很好的抽象。
感謝你能夠認真閱讀完這篇文章,希望小編分享的“Angular DOM中更新機制的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。