由模型變化觸發的 DOM 更新是全部前端框架的重要功能(注:即保持 model 和 view 的同步),固然 Angular 也不例外。定義一個以下模板表達式:前端
<span>Hello {{name}}</span>
複製代碼
或者相似下面的屬性綁定(注:這與上面代碼等價):node
<span [textContent]="'Hello ' + name"></span>
複製代碼
當每次 name
值發生變化時,Angular 會神奇般的自動更新 DOM 元素(注:最上面代碼是更新 DOM 文本節點,上面代碼是更新 DOM 元素節點,二者是不同的,下文解釋)。這表面上看起來很簡單,可是其內部工做至關複雜。並且,DOM 更新僅僅是 Angular 變動檢測機制 的一部分,變動檢測機制主要由如下三步組成:git
Input
bindings updates本文主要探索變動檢測機制的渲染部分(即 DOM updates 部分)。若是你以前也對這個問題很好奇,能夠繼續讀下去,絕對讓你茅塞頓開。github
在引用相關源碼時,假設程序是以生產模式運行。讓咱們開始吧!api
在探索 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)bash
組件工廠大部分代碼是由編譯器生成的不一樣視圖節點組成的,這些視圖節點是經過模板解析生成的(注:編譯器生成的組件工廠是一個返回值爲函數的函數,上文的 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.* 是第三個參數)。例如,下面的文本:
<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 操做類型:
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
是視圖節點的索引,若是你模板中有多個表達式:
<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);
複製代碼
如今咱們已經知道 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 語句
一步步去調試源碼。