摘要:注更新元素節(jié)點(diǎn)和文本節(jié)點(diǎn)都提到了渲染器,這也是一個(gè)重要的概念。每一個(gè)視圖對(duì)象都有一個(gè)屬性,即是的引用,也就是組件渲染器,的實(shí)際更新操作由它完成。
原文鏈接:The mechanics of DOM updates in Angular
由模型變化觸發(fā)的 DOM 更新是所有前端框架的重要功能(注:即保持 model 和 view 的同步),當(dāng)然 Angular 也不例外。定義一個(gè)如下模板表達(dá)式:
Hello {{name}}
或者類(lèi)似下面的屬性綁定(注:這與上面代碼等價(jià)):
當(dāng)每次 name 值發(fā)生變化時(shí),Angular 會(huì)神奇般的自動(dòng)更新 DOM 元素(注:最上面代碼是更新 DOM 文本節(jié)點(diǎn),上面代碼是更新 DOM 元素節(jié)點(diǎn),兩者是不一樣的,下文解釋?zhuān)?。這表面上看起來(lái)很簡(jiǎn)單,但是其內(nèi)部工作相當(dāng)復(fù)雜。而且,DOM 更新僅僅是 Angular 變更檢測(cè)機(jī)制 的一部分,變更檢測(cè)機(jī)制主要由以下三步組成:
DOM updates(注:即本文將要解釋的內(nèi)容)
child components Input bindings updates
query list updates
本文主要探索變更檢測(cè)機(jī)制的渲染部分(即 DOM updates 部分)。如果你之前也對(duì)這個(gè)問(wèn)題很好奇,可以繼續(xù)讀下去,絕對(duì)讓你茅塞頓開(kāi)。
在引用相關(guān)源碼時(shí),假設(shè)程序是以生產(chǎn)模式運(yùn)行。讓我們開(kāi)始吧!
程序內(nèi)部架構(gòu)在探索 DOM 更新之前,我們先搞清楚 Angular 程序內(nèi)部究竟是如何設(shè)計(jì)的,簡(jiǎn)單回顧下吧。
視圖從我的這篇文章 Here is what you need to know about dynamic components in?Angular 知道 Angular 編譯器會(huì)把程序中使用的組件編譯為一個(gè)工廠類(lèi)(factory)。例如,下面代碼展示 Angular 如何從工廠類(lèi)中創(chuàng)建一個(gè)組件(注:這里作者邏輯貌似有點(diǎn)亂,前一句說(shuō)的 Angular 編譯器編譯的工廠類(lèi),其實(shí)是編譯器去做的,不需要開(kāi)發(fā)者做任何事情,是自動(dòng)化的事情;而下面代碼說(shuō)的是開(kāi)發(fā)者如何手動(dòng)通過(guò) ComponentFactory 來(lái)創(chuàng)建一個(gè) Component 實(shí)例??傊?,他是想說(shuō)組件是怎么被實(shí)例化的):
const factory = r.resolveComponentFactory(AComponent); componentRef: ComponentRef= factory.create(injector);
Angular 使用這個(gè)工廠類(lèi)來(lái)實(shí)例化 View Definition ,然后使用 viewDef 函數(shù)來(lái) 創(chuàng)建視圖。Angular 內(nèi)部把一個(gè)程序看作為一顆視圖樹(shù),一個(gè)程序雖然有眾多組件,但有一個(gè)公共的視圖定義接口來(lái)定義由組件生成的視圖結(jié)構(gòu)(注:即 ViewDefinition Interface),當(dāng)然 Angular 使用每一個(gè)組件對(duì)象來(lái)創(chuàng)建對(duì)應(yīng)的視圖,從而由多個(gè)視圖組成視圖樹(shù)。(注:這里有一個(gè)主要概念就是視圖,其結(jié)構(gòu)就是 ViewDefinition Interface)
組件工廠組件工廠大部分代碼是由編譯器生成的不同視圖節(jié)點(diǎn)組成的,這些視圖節(jié)點(diǎn)是通過(guò)模板解析生成的(注:編譯器生成的組件工廠是一個(gè)返回值為函數(shù)的函數(shù),上文的 ComponentFactory 是 Angular 提供的類(lèi),供手動(dòng)調(diào)用。當(dāng)然,兩者指向同一個(gè)事物,只是表現(xiàn)形式不同而已)。假設(shè)定義一個(gè)組件的模板如下:
I am {{name}}
編譯器會(huì)解析這個(gè)模板生成包含如下類(lèi)似的組件工廠代碼(注:這只是最重要的部分代碼):
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),并在實(shí)例化組件時(shí)會(huì)被調(diào)用。jit_viewDef_1 其實(shí)就是 viewDef 函數(shù),用來(lái)創(chuàng)建視圖(注:viewDef 函數(shù)很重要,因?yàn)橐晥D是調(diào)用它創(chuàng)建的,生成的視圖結(jié)構(gòu)即是 ViewDefinition)。
viewDef 函數(shù)的第二個(gè)參數(shù) nodes 有些類(lèi)似 html 中節(jié)點(diǎn)的意思,但卻不僅僅如此。上面代碼中第二個(gè)參數(shù)是一個(gè)數(shù)組,其第一個(gè)數(shù)組元素 jit_elementDef_2 是元素節(jié)點(diǎn)定義,第二個(gè)數(shù)組元素 jit_textDef_3 是文本節(jié)點(diǎn)定義。Angular 編譯器會(huì)生成很多不同的節(jié)點(diǎn)定義,節(jié)點(diǎn)類(lèi)型是由 NodeFlags 設(shè)置的。稍后我們將看到 Angular 如何根據(jù)不同節(jié)點(diǎn)類(lèi)型來(lái)做 DOM 更新。
本文只對(duì)元素和文本節(jié)點(diǎn)感興趣:
export const enum NodeFlags { TypeElement = 1 << 0, TypeText = 1 << 1
讓我們簡(jiǎn)要擼一遍。
注:上文作者說(shuō)了一大段,其實(shí)核心就是,程序是一堆視圖組成的,而每一個(gè)視圖又是由不同類(lèi)型節(jié)點(diǎn)組成的。而本文只關(guān)心元素節(jié)點(diǎn)和文本節(jié)點(diǎn),至于還有個(gè)重要的指令節(jié)點(diǎn)在另一篇文章。元素節(jié)點(diǎn)的結(jié)構(gòu)定義
元素節(jié)點(diǎn)結(jié)構(gòu) 是 Angular 編譯每一個(gè) html 元素生成的節(jié)點(diǎn)結(jié)構(gòu),它也是用來(lái)生成組件的,如對(duì)這點(diǎn)感興趣可查看 Here is why you will not find components inside?Angular。元素節(jié)點(diǎn)也可以包含其他元素節(jié)點(diǎn)和文本節(jié)點(diǎn)作為子節(jié)點(diǎn),子節(jié)點(diǎn)數(shù)量是由 childCount 設(shè)置的。
所有元素定義是由 elementRef 函數(shù)生成的,而工廠函數(shù)中的 jit_elementDef_2() 就是這個(gè)函數(shù)。elementRef() 主要有以下幾個(gè)一般性參數(shù):
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 |
還有其他的幾個(gè)具有特定性能的參數(shù):
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 |
本文主要對(duì) bindings 感興趣。
注:從上文知道視圖(view)是由不同類(lèi)型節(jié)點(diǎn)(nodes)組成的,而元素節(jié)點(diǎn)(element nodes)是由 elementRef 函數(shù)生成的,元素節(jié)點(diǎn)的結(jié)構(gòu)是由 ElementDef 定義的。文本節(jié)點(diǎn)的結(jié)構(gòu)定義
文本節(jié)點(diǎn)結(jié)構(gòu) 是 Angular 編譯每一個(gè) html 文本 生成的節(jié)點(diǎn)結(jié)構(gòu)。通常它是元素定義節(jié)點(diǎn)的子節(jié)點(diǎn),就像我們本文的示例那樣(注:I am {{name}},span 是元素節(jié)點(diǎn),I am {{name}} 是文本節(jié)點(diǎn),也是 span 的子節(jié)點(diǎn))。這個(gè)文本節(jié)點(diǎn)是由 textDef 函數(shù)生成的。它的第二個(gè)參數(shù)以字符串?dāng)?shù)組形式傳進(jìn)來(lái)(注: Angular v5.* 是第三個(gè)參數(shù))。例如,下面的文本:
Hello {{name}} and another {{prop}}
將要被解析為一個(gè)數(shù)組:
["Hello ", " and another ", ""]
然后被用來(lái)生成正確的綁定:
{ text: "Hello", bindings: [ { name: "name", suffix: " and another " }, { name: "prop", suffix: "" } ] }
在臟檢查(注:即變更檢測(cè))階段會(huì)這么用來(lái)生成文本:
text + context[bindings[0][property]] + context[bindings[0][suffix]] + context[bindings[1][property]] + context[bindings[1][suffix]]
注:同上,文本節(jié)點(diǎn)是由 textDef 函數(shù)生成的,結(jié)構(gòu)是由 TextDef 定義的。既然已經(jīng)知道了兩個(gè)節(jié)點(diǎn)的定義和生成,那節(jié)點(diǎn)上的屬性綁定, Angular 是怎么處理的呢?節(jié)點(diǎn)的綁定
Angular 使用 BindingDef 來(lái)定義每一個(gè)節(jié)點(diǎn)的綁定依賴(lài),而這些綁定依賴(lài)通常是組件類(lèi)的屬性。在變更檢測(cè)時(shí) Angular 會(huì)根據(jù)這些綁定來(lái)決定如何更新節(jié)點(diǎn)和提供上下文信息。具體哪一種操作是由 BindingFlags 決定的,下面列表展示了具體的 DOM 操作類(lèi)型:
Name | Construction in template |
---|---|
TypeElementAttribute | attr.name |
TypeElementClass | class.name |
TypeElementStyle | style.name |
元素和文本定義根據(jù)這些編譯器可識(shí)別的綁定標(biāo)志位,內(nèi)部創(chuàng)建這些綁定依賴(lài)。每一種節(jié)點(diǎn)類(lèi)型都有著不同的綁定生成邏輯(注:意思是 Angular 會(huì)根據(jù) BindingFlags 來(lái)生成對(duì)應(yīng)的 BindingDef)。
更新渲染器最讓我們感興趣的是 jit_viewDef_1 中最后那個(gè)參數(shù):
function(_ck,_v) { var _co = _v.component; var currVal_0 = _co.name; _ck(_v,1,0,currVal_0); });
這個(gè)函數(shù)叫做 updateRenderer。它接收兩個(gè)參數(shù):_ck 和 _v。_ck 是 check 的簡(jiǎn)寫(xiě),其實(shí)就是 prodCheckAndUpdateNode 函數(shù),而 _v 就是當(dāng)前視圖對(duì)象。updateRenderer 函數(shù)會(huì)在 每一次變更檢測(cè)時(shí) 被調(diào)用,其參數(shù) _ck 和 _v 也是這時(shí)被傳入。
updateRenderer 函數(shù)邏輯主要是,從組件對(duì)象的綁定屬性獲取當(dāng)前值,并調(diào)用 _ck 函數(shù),同時(shí)傳入視圖對(duì)象、視圖節(jié)點(diǎn)索引和綁定屬性當(dāng)前值。重要一點(diǎn)是 Angular 會(huì)為每一個(gè)視圖執(zhí)行 DOM 更新操作,所以必須傳入視圖節(jié)點(diǎn)索引參數(shù)(注:這個(gè)很好理解,上文說(shuō)了 Angular 會(huì)依次對(duì)每一個(gè) view 做模型視圖同步過(guò)程)。你可以清晰看到 _ck 參數(shù)列表:
function prodCheckAndUpdateNode( view: ViewData, nodeIndex: number, argStyle: ArgumentType, v0?: any, v1?: any, v2?: any,
nodeIndex 是視圖節(jié)點(diǎn)的索引,如果你模板中有多個(gè)表達(dá)式:
Hello {{name}}
Hello {{age}}
編譯器生成的 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 編譯器生成的所有對(duì)象(注:已經(jīng)有了 view,element node,text node 和 updateRenderer 這幾個(gè)道具),現(xiàn)在我們可以探索如何使用這些對(duì)象來(lái)更新 DOM。
從上文我們知道變更檢測(cè)期間 updateRenderer 函數(shù)傳入的一個(gè)參數(shù)是 _ck 函數(shù),而這個(gè)函數(shù)就是 prodCheckAndUpdateNode。這個(gè)函數(shù)在繼續(xù)執(zhí)行后,最終會(huì)調(diào)用 checkAndUpdateNodeInline ,如果綁定屬性的數(shù)量超過(guò) 10,Angular 還提供了 checkAndUpdateNodeDynamic 這個(gè)函數(shù)(注:兩個(gè)函數(shù)本質(zhì)一樣)。
checkAndUpdateNodeInline 函數(shù)會(huì)根據(jù)不同視圖節(jié)點(diǎn)類(lèi)型來(lái)執(zhí)行對(duì)應(yīng)的檢查更新函數(shù):
case NodeFlags.TypeElement -> checkAndUpdateElementInline case NodeFlags.TypeText -> checkAndUpdateTextInline case NodeFlags.TypeDirective -> checkAndUpdateDirectiveInline
讓我們看下這些函數(shù)是做什么的,至于 NodeFlags.TypeDirective 可以查看我寫(xiě)的文章 The mechanics of property bindings update in Angular 。
注:因?yàn)楸疚闹魂P(guān)注 element node 和 text node。元素節(jié)點(diǎn)
對(duì)于元素節(jié)點(diǎn),會(huì)調(diào)用函數(shù) checkAndUpdateElementInline 以及 checkAndUpdateElementValue,checkAndUpdateElementValue 函數(shù)會(huì)檢查綁定形式是否是 [attr.name, class.name, style.some] 或是屬性綁定形式:
case BindingFlags.TypeElementAttribute -> setElementAttribute case BindingFlags.TypeElementClass -> setElementClass case BindingFlags.TypeElementStyle -> setElementStyle case BindingFlags.TypeProperty -> setElementProperty;
然后使用渲染器對(duì)應(yīng)的方法來(lái)對(duì)該節(jié)點(diǎn)執(zhí)行對(duì)應(yīng)操作,比如使用 setElementClass 給當(dāng)前節(jié)點(diǎn) span 添加一個(gè) class。
文本節(jié)點(diǎn)對(duì)于文本節(jié)點(diǎn)類(lèi)型,會(huì)調(diào)用 checkAndUpdateTextInline ,下面是主要部分:
if (checkAndUpdateBinding(view, nodeDef, bindingIndex, newValue)) { value = text + _addInterpolationPart(...); view.renderer.setValue(DOMNode, value); }
它會(huì)拿到 updateRenderer 函數(shù)傳過(guò)來(lái)的當(dāng)前值(注:即上文的 _ck(_v,4,0,currVal_1);),與上一次變更檢測(cè)時(shí)的值相比較。視圖數(shù)據(jù)包含有 oldValues 屬性,如果屬性值如 name 發(fā)生變化,Angular 會(huì)使用最新 name 值合成最新的字符串文本,如 Hello New World,然后使用渲染器更新 DOM 上對(duì)應(yīng)的文本。
注:更新元素節(jié)點(diǎn)和文本節(jié)點(diǎn)都提到了渲染器(renderer),這也是一個(gè)重要的概念。每一個(gè)視圖對(duì)象都有一個(gè) renderer 屬性,即是 Renderer2 的引用,也就是組件渲染器,DOM 的實(shí)際更新操作由它完成。因?yàn)?Angular 是跨平臺(tái)的,這個(gè) Renderer2 是個(gè)接口,這樣根據(jù)不同 Platform 就選擇不同的 Renderer。比如,在瀏覽器里這個(gè) Renderer 就是 DOMRenderer,在服務(wù)端就是 ServerRenderer,等等。從這里可看出,Angular 框架設(shè)計(jì)做了很好的抽象。結(jié)論
我知道有大量難懂的信息需要消化,但是只要理解了這些知識(shí),你就可以更好的設(shè)計(jì)程序或者去調(diào)試 DOM 更新相關(guān)的問(wèn)題。我建議你按照本文提到的源碼邏輯,使用調(diào)試器或 debugger 語(yǔ)句 一步步去調(diào)試源碼。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/107723.html
摘要:本文主要介紹輸入輸出綁定方式,特別是當(dāng)父組件輸入綁定值變化時(shí),如何更新子組件輸入值。更新指令的屬性上文中已經(jīng)描述了函數(shù)是用來(lái)更新元素的屬性,而是用來(lái)更新子組件的輸入綁定屬性,并且變更檢測(cè)期間傳入的參數(shù)就是函數(shù)。 原文鏈接:The mechanics of property bindings update in Angular showImg(https://segmentfault....
摘要:但如果一個(gè)組件在生命周期鉤子里改變父組件屬性,卻是可以的,因?yàn)檫@個(gè)鉤子函數(shù)是在更新父組件屬性變化之前調(diào)用的注即第步,在第步之前調(diào)用。 原文鏈接:Angular.js’ $digest is reborn in the newer version of Angular showImg(https://segmentfault.com/img/remote/146000001468785...
摘要:本質(zhì)上,本文主要解釋內(nèi)部是如何定義組件和指令的,并引入新的視圖節(jié)點(diǎn)定義指令定義。大多數(shù)指令使用屬性選擇器,但是有一些也選擇元素選擇器。實(shí)際上,表單指令就是使用元素選擇器來(lái)把特定行為附著在元素上。但是由于編譯器會(huì)為每一個(gè) 原文鏈接:Here is why you will not find components inside Angular showImg(https://segmen...
摘要:本文將解釋引起這個(gè)錯(cuò)誤的內(nèi)在原因,檢測(cè)機(jī)制的內(nèi)部原理,提供導(dǎo)致這個(gè)錯(cuò)誤的共同行為,并給出修復(fù)這個(gè)錯(cuò)誤的解決方案。這一次過(guò)程稱(chēng)為。這個(gè)程序設(shè)計(jì)為子組件拋出一個(gè)事件,而父組件監(jiān)聽(tīng)這個(gè)事件,而這個(gè)事件會(huì)引起父組件屬性值發(fā)生改變。 原文鏈接:Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedE...
摘要:所以,單向數(shù)據(jù)流的意思是指在變更檢測(cè)期間屬性綁定變更的架構(gòu)。相反,輸出綁定過(guò)程并沒(méi)有在變更檢測(cè)期間內(nèi)運(yùn)行,所以它沒(méi)有把單向數(shù)據(jù)流轉(zhuǎn)變?yōu)殡p向數(shù)據(jù)流。說(shuō)的單向數(shù)據(jù)流說(shuō)的是服務(wù)層,而不是視圖層嗷。 原文鏈接: Do you really know what unidirectional data flow means in?Angular 關(guān)于單向數(shù)據(jù)流,還可以參考這篇文章,且文中還有 y...
閱讀 4067·2021-11-18 13:22
閱讀 1860·2021-11-17 09:33
閱讀 2903·2021-09-26 09:46
閱讀 1237·2021-08-21 14:11
閱讀 2912·2019-08-30 15:53
閱讀 2731·2019-08-30 15:52
閱讀 1959·2019-08-30 10:52
閱讀 1542·2019-08-29 15:30