摘要:注更新元素節點和文本節點都提到了渲染器,這也是一個重要的概念。每一個視圖對象都有一個屬性,即是的引用,也就是組件渲染器,的實際更新操作由它完成。
原文鏈接:The mechanics of DOM updates in Angular
由模型變化觸發的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 view 的同步),當然 Angular 也不例外。定義一個如下模板表達式:
Hello {{name}}
或者類似下面的屬性綁定(注:這與上面代碼等價):
當每次 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= factory.create(injector);
Angular 使用這個工廠類來實例化 View Definition ,然后使用 viewDef 函數來 創建視圖。Angular 內部把一個程序看作為一顆視圖樹,一個程序雖然有眾多組件,但有一個公共的視圖定義接口來定義由組件生成的視圖結構(注:即 ViewDefinition Interface),當然 Angular 使用每一個組件對象來創建對應的視圖,從而由多個視圖組成視圖樹。(注:這里有一個主要概念就是視圖,其結構就是 ViewDefinition Interface)
組件工廠組件工廠大部分代碼是由編譯器生成的不同視圖節點組成的,這些視圖節點是通過模板解析生成的(注:編譯器生成的組件工廠是一個返回值為函數的函數,上文的 ComponentFactory 是 Angular 提供的類,供手動調用。當然,兩者指向同一個事物,只是表現形式不同而已)。假設定義一個組件的模板如下:
I am {{name}}
編譯器會解析這個模板生成包含如下類似的組件工廠代碼(注:這只是最重要的部分代碼):
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 文本 生成的節點結構。通常它是元素定義節點的子節點,就像我們本文的示例那樣(注:I am {{name}},span 是元素節點,I am {{name}} 是文本節點,也是 span 的子節點)。這個文本節點是由 textDef 函數生成的。它的第二個參數以字符串數組形式傳進來(注: Angular v5.* 是第三個參數)。例如,下面的文本:
Hello {{name}} and another {{prop}}
將要被解析為一個數組:
["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 是視圖節點的索引,如果你模板中有多個表達式:
Hello {{name}}
Hello {{age}}
編譯器生成的 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 以及 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 框架設計做了很好的抽象。結論
我知道有大量難懂的信息需要消化,但是只要理解了這些知識,你就可以更好的設計程序或者去調試 DOM 更新相關的問題。我建議你按照本文提到的源碼邏輯,使用調試器或 debugger 語句 一步步去調試源碼。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107723.html
摘要:本文主要介紹輸入輸出綁定方式,特別是當父組件輸入綁定值變化時,如何更新子組件輸入值。更新指令的屬性上文中已經描述了函數是用來更新元素的屬性,而是用來更新子組件的輸入綁定屬性,并且變更檢測期間傳入的參數就是函數。 原文鏈接:The mechanics of property bindings update in Angular showImg(https://segmentfault....
摘要:但如果一個組件在生命周期鉤子里改變父組件屬性,卻是可以的,因為這個鉤子函數是在更新父組件屬性變化之前調用的注即第步,在第步之前調用。 原文鏈接:Angular.js’ $digest is reborn in the newer version of Angular showImg(https://segmentfault.com/img/remote/146000001468785...
摘要:本質上,本文主要解釋內部是如何定義組件和指令的,并引入新的視圖節點定義指令定義。大多數指令使用屬性選擇器,但是有一些也選擇元素選擇器。實際上,表單指令就是使用元素選擇器來把特定行為附著在元素上。但是由于編譯器會為每一個 原文鏈接:Here is why you will not find components inside Angular showImg(https://segmen...
摘要:本文將解釋引起這個錯誤的內在原因,檢測機制的內部原理,提供導致這個錯誤的共同行為,并給出修復這個錯誤的解決方案。這一次過程稱為。這個程序設計為子組件拋出一個事件,而父組件監聽這個事件,而這個事件會引起父組件屬性值發生改變。 原文鏈接:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedE...
摘要:所以,單向數據流的意思是指在變更檢測期間屬性綁定變更的架構。相反,輸出綁定過程并沒有在變更檢測期間內運行,所以它沒有把單向數據流轉變為雙向數據流。說的單向數據流說的是服務層,而不是視圖層嗷。 原文鏈接: Do you really know what unidirectional data flow means in?Angular 關于單向數據流,還可以參考這篇文章,且文中還有 y...
閱讀 3976·2021-11-18 13:22
閱讀 1813·2021-11-17 09:33
閱讀 2877·2021-09-26 09:46
閱讀 1208·2021-08-21 14:11
閱讀 2884·2019-08-30 15:53
閱讀 2707·2019-08-30 15:52
閱讀 1885·2019-08-30 10:52
閱讀 1517·2019-08-29 15:30